mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-03-02 06:10:18 +01:00
Merge branch 'main' into added-codeql
This commit is contained in:
commit
4b72518813
255 changed files with 20660 additions and 41548 deletions
213
.eslintrc.js
213
.eslintrc.js
|
|
@ -1,213 +0,0 @@
|
||||||
module.exports = {
|
|
||||||
env: {
|
|
||||||
browser: true,
|
|
||||||
es2021: true,
|
|
||||||
node: true,
|
|
||||||
commonjs: true,
|
|
||||||
es6: true,
|
|
||||||
},
|
|
||||||
extends: [
|
|
||||||
'eslint:recommended',
|
|
||||||
'plugin:react/recommended',
|
|
||||||
'plugin:react-hooks/recommended',
|
|
||||||
'plugin:jest/recommended',
|
|
||||||
'prettier',
|
|
||||||
'plugin:jsx-a11y/recommended',
|
|
||||||
],
|
|
||||||
ignorePatterns: [
|
|
||||||
'client/dist/**/*',
|
|
||||||
'client/public/**/*',
|
|
||||||
'e2e/playwright-report/**/*',
|
|
||||||
'packages/mcp/types/**/*',
|
|
||||||
'packages/mcp/dist/**/*',
|
|
||||||
'packages/mcp/test_bundle/**/*',
|
|
||||||
'api/demo/**/*',
|
|
||||||
'packages/data-provider/types/**/*',
|
|
||||||
'packages/data-provider/dist/**/*',
|
|
||||||
'packages/data-provider/test_bundle/**/*',
|
|
||||||
'data-node/**/*',
|
|
||||||
'meili_data/**/*',
|
|
||||||
'node_modules/**/*',
|
|
||||||
],
|
|
||||||
parser: '@typescript-eslint/parser',
|
|
||||||
parserOptions: {
|
|
||||||
ecmaVersion: 'latest',
|
|
||||||
sourceType: 'module',
|
|
||||||
ecmaFeatures: {
|
|
||||||
jsx: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
plugins: ['react', 'react-hooks', '@typescript-eslint', 'import', 'jsx-a11y'],
|
|
||||||
rules: {
|
|
||||||
'react/react-in-jsx-scope': 'off',
|
|
||||||
'@typescript-eslint/ban-ts-comment': ['error', { 'ts-ignore': 'allow' }],
|
|
||||||
indent: ['error', 2, { SwitchCase: 1 }],
|
|
||||||
'max-len': [
|
|
||||||
'error',
|
|
||||||
{
|
|
||||||
code: 120,
|
|
||||||
ignoreStrings: true,
|
|
||||||
ignoreTemplateLiterals: true,
|
|
||||||
ignoreComments: true,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
'linebreak-style': 0,
|
|
||||||
curly: ['error', 'all'],
|
|
||||||
semi: ['error', 'always'],
|
|
||||||
'object-curly-spacing': ['error', 'always'],
|
|
||||||
'no-multiple-empty-lines': ['error', { max: 1 }],
|
|
||||||
'no-trailing-spaces': 'error',
|
|
||||||
'comma-dangle': ['error', 'always-multiline'],
|
|
||||||
// "arrow-parens": [2, "as-needed", { requireForBlockBody: true }],
|
|
||||||
// 'no-plusplus': ['error', { allowForLoopAfterthoughts: true }],
|
|
||||||
'no-console': 'off',
|
|
||||||
'import/no-cycle': 'error',
|
|
||||||
'import/no-self-import': 'error',
|
|
||||||
'import/extensions': 'off',
|
|
||||||
'no-promise-executor-return': 'off',
|
|
||||||
'no-param-reassign': 'off',
|
|
||||||
'no-continue': 'off',
|
|
||||||
'no-restricted-syntax': 'off',
|
|
||||||
'react/prop-types': ['off'],
|
|
||||||
'react/display-name': ['off'],
|
|
||||||
'no-nested-ternary': 'error',
|
|
||||||
'no-unused-vars': ['error', { varsIgnorePattern: '^_' }],
|
|
||||||
quotes: ['error', 'single'],
|
|
||||||
},
|
|
||||||
overrides: [
|
|
||||||
{
|
|
||||||
files: ['**/*.ts', '**/*.tsx'],
|
|
||||||
rules: {
|
|
||||||
'no-unused-vars': 'off', // off because it conflicts with '@typescript-eslint/no-unused-vars'
|
|
||||||
'react/display-name': 'off',
|
|
||||||
'@typescript-eslint/no-unused-vars': 'warn',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
files: ['rollup.config.js', '.eslintrc.js', 'jest.config.js'],
|
|
||||||
env: {
|
|
||||||
node: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
files: [
|
|
||||||
'**/*.test.js',
|
|
||||||
'**/*.test.jsx',
|
|
||||||
'**/*.test.ts',
|
|
||||||
'**/*.test.tsx',
|
|
||||||
'**/*.spec.js',
|
|
||||||
'**/*.spec.jsx',
|
|
||||||
'**/*.spec.ts',
|
|
||||||
'**/*.spec.tsx',
|
|
||||||
'setupTests.js',
|
|
||||||
],
|
|
||||||
env: {
|
|
||||||
jest: true,
|
|
||||||
node: true,
|
|
||||||
},
|
|
||||||
rules: {
|
|
||||||
'react/display-name': 'off',
|
|
||||||
'react/prop-types': 'off',
|
|
||||||
'react/no-unescaped-entities': 'off',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
files: ['**/*.ts', '**/*.tsx'],
|
|
||||||
parser: '@typescript-eslint/parser',
|
|
||||||
parserOptions: {
|
|
||||||
project: './client/tsconfig.json',
|
|
||||||
},
|
|
||||||
plugins: ['@typescript-eslint/eslint-plugin', 'jest'],
|
|
||||||
extends: [
|
|
||||||
'plugin:@typescript-eslint/eslint-recommended',
|
|
||||||
'plugin:@typescript-eslint/recommended',
|
|
||||||
],
|
|
||||||
rules: {
|
|
||||||
'@typescript-eslint/no-explicit-any': 'error',
|
|
||||||
'@typescript-eslint/no-unnecessary-condition': 'warn',
|
|
||||||
'@typescript-eslint/strict-boolean-expressions': 'warn',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
files: './packages/data-provider/**/*.ts',
|
|
||||||
overrides: [
|
|
||||||
{
|
|
||||||
files: '**/*.ts',
|
|
||||||
parser: '@typescript-eslint/parser',
|
|
||||||
parserOptions: {
|
|
||||||
project: './packages/data-provider/tsconfig.json',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
files: './api/demo/**/*.ts',
|
|
||||||
overrides: [
|
|
||||||
{
|
|
||||||
files: '**/*.ts',
|
|
||||||
parser: '@typescript-eslint/parser',
|
|
||||||
parserOptions: {
|
|
||||||
project: './packages/data-provider/tsconfig.json',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
files: './packages/mcp/**/*.ts',
|
|
||||||
overrides: [
|
|
||||||
{
|
|
||||||
files: '**/*.ts',
|
|
||||||
parser: '@typescript-eslint/parser',
|
|
||||||
parserOptions: {
|
|
||||||
project: './packages/mcp/tsconfig.json',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
files: './config/translations/**/*.ts',
|
|
||||||
parser: '@typescript-eslint/parser',
|
|
||||||
parserOptions: {
|
|
||||||
project: './config/translations/tsconfig.json',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
files: ['./packages/data-provider/specs/**/*.ts'],
|
|
||||||
parserOptions: {
|
|
||||||
project: './packages/data-provider/tsconfig.spec.json',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
files: ['./api/demo/specs/**/*.ts'],
|
|
||||||
parserOptions: {
|
|
||||||
project: './packages/data-provider/tsconfig.spec.json',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
files: ['./packages/mcp/specs/**/*.ts'],
|
|
||||||
parserOptions: {
|
|
||||||
project: './packages/mcp/tsconfig.spec.json',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
settings: {
|
|
||||||
react: {
|
|
||||||
createClass: 'createReactClass', // Regex for Component Factory to use,
|
|
||||||
// default to "createReactClass"
|
|
||||||
pragma: 'React', // Pragma to use, default to "React"
|
|
||||||
fragment: 'Fragment', // Fragment to use (may be a property of <pragma>), default to "Fragment"
|
|
||||||
version: 'detect', // React version. "detect" automatically picks the version you have installed.
|
|
||||||
},
|
|
||||||
'import/parsers': {
|
|
||||||
'@typescript-eslint/parser': ['.ts', '.tsx'],
|
|
||||||
},
|
|
||||||
'import/resolver': {
|
|
||||||
typescript: {
|
|
||||||
project: ['./client/tsconfig.json'],
|
|
||||||
},
|
|
||||||
node: {
|
|
||||||
project: ['./client/tsconfig.json'],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
42
.github/ISSUE_TEMPLATE/BUG-REPORT.yml
vendored
42
.github/ISSUE_TEMPLATE/BUG-REPORT.yml
vendored
|
|
@ -1,12 +1,19 @@
|
||||||
name: Bug Report
|
name: Bug Report
|
||||||
description: File a bug report
|
description: File a bug report
|
||||||
title: "[Bug]: "
|
title: "[Bug]: "
|
||||||
labels: ["bug"]
|
labels: ["🐛 bug"]
|
||||||
body:
|
body:
|
||||||
- type: markdown
|
- type: markdown
|
||||||
attributes:
|
attributes:
|
||||||
value: |
|
value: |
|
||||||
Thanks for taking the time to fill out this bug report!
|
Thanks for taking the time to fill out this bug report!
|
||||||
|
|
||||||
|
Before submitting, please:
|
||||||
|
- Search existing [Issues and Discussions](https://github.com/danny-avila/LibreChat/discussions) to see if your bug has already been reported
|
||||||
|
- Use [Discussions](https://github.com/danny-avila/LibreChat/discussions) instead of Issues for:
|
||||||
|
- General inquiries
|
||||||
|
- Help with setup
|
||||||
|
- Questions about whether you're experiencing a bug
|
||||||
- type: textarea
|
- type: textarea
|
||||||
id: what-happened
|
id: what-happened
|
||||||
attributes:
|
attributes:
|
||||||
|
|
@ -15,6 +22,23 @@ body:
|
||||||
placeholder: Please give as many details as possible
|
placeholder: Please give as many details as possible
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
id: version-info
|
||||||
|
attributes:
|
||||||
|
label: Version Information
|
||||||
|
description: |
|
||||||
|
If using Docker, please run and provide the output of:
|
||||||
|
```bash
|
||||||
|
docker images | grep librechat
|
||||||
|
```
|
||||||
|
|
||||||
|
If running from source, please run and provide the output of:
|
||||||
|
```bash
|
||||||
|
git rev-parse HEAD
|
||||||
|
```
|
||||||
|
placeholder: Paste the output here
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
- type: textarea
|
- type: textarea
|
||||||
id: steps-to-reproduce
|
id: steps-to-reproduce
|
||||||
attributes:
|
attributes:
|
||||||
|
|
@ -39,7 +63,21 @@ body:
|
||||||
id: logs
|
id: logs
|
||||||
attributes:
|
attributes:
|
||||||
label: Relevant log output
|
label: Relevant log output
|
||||||
description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks.
|
description: |
|
||||||
|
Please paste relevant logs that were created when reproducing the error.
|
||||||
|
|
||||||
|
Log locations:
|
||||||
|
- Docker: Project root directory ./logs
|
||||||
|
- npm: ./api/logs
|
||||||
|
|
||||||
|
There are two types of logs that can help diagnose the issue:
|
||||||
|
- debug logs (debug-YYYY-MM-DD.log)
|
||||||
|
- error logs (error-YYYY-MM-DD.log)
|
||||||
|
|
||||||
|
Error logs contain exact stack traces and are especially helpful, but both can provide valuable information.
|
||||||
|
Please only include the relevant portions of logs that correspond to when you reproduced the error.
|
||||||
|
|
||||||
|
For UI-related issues, browser console logs can be very helpful. You can provide these as screenshots or paste the text here.
|
||||||
render: shell
|
render: shell
|
||||||
- type: textarea
|
- type: textarea
|
||||||
id: screenshots
|
id: screenshots
|
||||||
|
|
|
||||||
4
.github/ISSUE_TEMPLATE/FEATURE-REQUEST.yml
vendored
4
.github/ISSUE_TEMPLATE/FEATURE-REQUEST.yml
vendored
|
|
@ -1,7 +1,7 @@
|
||||||
name: Feature Request
|
name: Feature Request
|
||||||
description: File a feature request
|
description: File a feature request
|
||||||
title: "Enhancement: "
|
title: "[Enhancement]: "
|
||||||
labels: ["enhancement"]
|
labels: ["✨ enhancement"]
|
||||||
body:
|
body:
|
||||||
- type: markdown
|
- type: markdown
|
||||||
attributes:
|
attributes:
|
||||||
|
|
|
||||||
33
.github/ISSUE_TEMPLATE/NEW-LANGUAGE-REQUEST.yml
vendored
Normal file
33
.github/ISSUE_TEMPLATE/NEW-LANGUAGE-REQUEST.yml
vendored
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
name: New Language Request
|
||||||
|
description: Request to add a new language for LibreChat translations.
|
||||||
|
title: "New Language Request: "
|
||||||
|
labels: ["✨ enhancement", "🌍 i18n"]
|
||||||
|
body:
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
Thank you for taking the time to submit a new language request! Please fill out the following details so we can review your request.
|
||||||
|
- type: input
|
||||||
|
id: language_name
|
||||||
|
attributes:
|
||||||
|
label: Language Name
|
||||||
|
description: Please provide the full name of the language (e.g., Spanish, Mandarin).
|
||||||
|
placeholder: e.g., Spanish
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: input
|
||||||
|
id: iso_code
|
||||||
|
attributes:
|
||||||
|
label: ISO 639-1 Code
|
||||||
|
description: Please provide the ISO 639-1 code for the language (e.g., es for Spanish). You can refer to [this list](https://www.w3schools.com/tags/ref_language_codes.asp) for valid codes.
|
||||||
|
placeholder: e.g., es
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: checkboxes
|
||||||
|
id: terms
|
||||||
|
attributes:
|
||||||
|
label: Code of Conduct
|
||||||
|
description: By submitting this issue, you agree to follow our [Code of Conduct](https://github.com/danny-avila/LibreChat/blob/main/.github/CODE_OF_CONDUCT.md).
|
||||||
|
options:
|
||||||
|
- label: I agree to follow this project's Code of Conduct
|
||||||
|
required: true
|
||||||
2
.github/ISSUE_TEMPLATE/QUESTION.yml
vendored
2
.github/ISSUE_TEMPLATE/QUESTION.yml
vendored
|
|
@ -1,7 +1,7 @@
|
||||||
name: Question
|
name: Question
|
||||||
description: Ask your question
|
description: Ask your question
|
||||||
title: "[Question]: "
|
title: "[Question]: "
|
||||||
labels: ["question"]
|
labels: ["❓ question"]
|
||||||
body:
|
body:
|
||||||
- type: markdown
|
- type: markdown
|
||||||
attributes:
|
attributes:
|
||||||
|
|
|
||||||
5
.github/workflows/backend-review.yml
vendored
5
.github/workflows/backend-review.yml
vendored
|
|
@ -62,8 +62,3 @@ jobs:
|
||||||
|
|
||||||
- name: Run librechat-data-provider unit tests
|
- name: Run librechat-data-provider unit tests
|
||||||
run: cd packages/data-provider && npm run test:ci
|
run: cd packages/data-provider && npm run test:ci
|
||||||
|
|
||||||
- name: Run linters
|
|
||||||
uses: wearerequired/lint-action@v2
|
|
||||||
with:
|
|
||||||
eslint: true
|
|
||||||
|
|
|
||||||
73
.github/workflows/eslint-ci.yml
vendored
Normal file
73
.github/workflows/eslint-ci.yml
vendored
Normal file
|
|
@ -0,0 +1,73 @@
|
||||||
|
name: ESLint Code Quality Checks
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
- dev
|
||||||
|
- release/*
|
||||||
|
paths:
|
||||||
|
- 'api/**'
|
||||||
|
- 'client/**'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
eslint_checks:
|
||||||
|
name: Run ESLint Linting
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
security-events: write
|
||||||
|
actions: read
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Set up Node.js 20.x
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 20
|
||||||
|
cache: npm
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: npm ci
|
||||||
|
|
||||||
|
# Run ESLint on changed files within the api/ and client/ directories.
|
||||||
|
- name: Run ESLint on changed files
|
||||||
|
env:
|
||||||
|
SARIF_ESLINT_IGNORE_SUPPRESSED: "true"
|
||||||
|
run: |
|
||||||
|
# Extract the base commit SHA from the pull_request event payload.
|
||||||
|
BASE_SHA=$(jq --raw-output .pull_request.base.sha "$GITHUB_EVENT_PATH")
|
||||||
|
echo "Base commit SHA: $BASE_SHA"
|
||||||
|
|
||||||
|
# Get changed files (only JS/TS files in api/ or client/)
|
||||||
|
CHANGED_FILES=$(git diff --name-only --diff-filter=ACMRTUXB "$BASE_SHA" HEAD | grep -E '^(api|client)/.*\.(js|jsx|ts|tsx)$' || true)
|
||||||
|
|
||||||
|
# Debug output
|
||||||
|
echo "Changed files:"
|
||||||
|
echo "$CHANGED_FILES"
|
||||||
|
|
||||||
|
# Ensure there are files to lint before running ESLint
|
||||||
|
if [[ -z "$CHANGED_FILES" ]]; then
|
||||||
|
echo "No matching files changed. Skipping ESLint."
|
||||||
|
echo "UPLOAD_SARIF=false" >> $GITHUB_ENV
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Set variable to allow SARIF upload
|
||||||
|
echo "UPLOAD_SARIF=true" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
# Run ESLint
|
||||||
|
npx eslint --no-error-on-unmatched-pattern \
|
||||||
|
--config eslint.config.mjs \
|
||||||
|
--format @microsoft/eslint-formatter-sarif \
|
||||||
|
--output-file eslint-results.sarif $CHANGED_FILES || true
|
||||||
|
|
||||||
|
- name: Upload analysis results to GitHub
|
||||||
|
if: env.UPLOAD_SARIF == 'true'
|
||||||
|
uses: github/codeql-action/upload-sarif@v3
|
||||||
|
with:
|
||||||
|
sarif_file: eslint-results.sarif
|
||||||
|
wait-for-processing: true
|
||||||
84
.github/workflows/i18n-unused-keys.yml
vendored
Normal file
84
.github/workflows/i18n-unused-keys.yml
vendored
Normal file
|
|
@ -0,0 +1,84 @@
|
||||||
|
name: Detect Unused i18next Strings
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
paths:
|
||||||
|
- "client/src/**"
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
detect-unused-i18n-keys:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
pull-requests: write # Required for posting PR comments
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Find unused i18next keys
|
||||||
|
id: find-unused
|
||||||
|
run: |
|
||||||
|
echo "🔍 Scanning for unused i18next keys..."
|
||||||
|
|
||||||
|
# Define paths
|
||||||
|
I18N_FILE="client/src/locales/en/translation.json"
|
||||||
|
SOURCE_DIR="client/src"
|
||||||
|
|
||||||
|
# Check if translation file exists
|
||||||
|
if [[ ! -f "$I18N_FILE" ]]; then
|
||||||
|
echo "::error title=Missing i18n File::Translation file not found: $I18N_FILE"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Extract all keys from the JSON file
|
||||||
|
KEYS=$(jq -r 'keys[]' "$I18N_FILE")
|
||||||
|
|
||||||
|
# Track unused keys
|
||||||
|
UNUSED_KEYS=()
|
||||||
|
|
||||||
|
# Check if each key is used in the source code
|
||||||
|
for KEY in $KEYS; do
|
||||||
|
if ! grep -r --include=\*.{js,jsx,ts,tsx} -q "$KEY" "$SOURCE_DIR"; then
|
||||||
|
UNUSED_KEYS+=("$KEY")
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# Output results
|
||||||
|
if [[ ${#UNUSED_KEYS[@]} -gt 0 ]]; then
|
||||||
|
echo "🛑 Found ${#UNUSED_KEYS[@]} unused i18n keys:"
|
||||||
|
echo "unused_keys=$(echo "${UNUSED_KEYS[@]}" | jq -R -s -c 'split(" ")')" >> $GITHUB_ENV
|
||||||
|
for KEY in "${UNUSED_KEYS[@]}"; do
|
||||||
|
echo "::warning title=Unused i18n Key::'$KEY' is defined but not used in the codebase."
|
||||||
|
done
|
||||||
|
else
|
||||||
|
echo "✅ No unused i18n keys detected!"
|
||||||
|
echo "unused_keys=[]" >> $GITHUB_ENV
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Post verified comment on PR
|
||||||
|
if: env.unused_keys != '[]'
|
||||||
|
run: |
|
||||||
|
PR_NUMBER=$(jq --raw-output .pull_request.number "$GITHUB_EVENT_PATH")
|
||||||
|
|
||||||
|
# Format the unused keys list correctly, filtering out empty entries
|
||||||
|
FILTERED_KEYS=$(echo "$unused_keys" | jq -r '.[]' | grep -v '^\s*$' | sed 's/^/- `/;s/$/`/' )
|
||||||
|
|
||||||
|
COMMENT_BODY=$(cat <<EOF
|
||||||
|
### 🚨 Unused i18next Keys Detected
|
||||||
|
|
||||||
|
The following translation keys are defined in \`translation.json\` but are **not used** in the codebase:
|
||||||
|
|
||||||
|
$FILTERED_KEYS
|
||||||
|
|
||||||
|
⚠️ **Please remove these unused keys to keep the translation files clean.**
|
||||||
|
EOF
|
||||||
|
)
|
||||||
|
|
||||||
|
gh api "repos/${{ github.repository }}/issues/${PR_NUMBER}/comments" \
|
||||||
|
-f body="$COMMENT_BODY" \
|
||||||
|
-H "Authorization: token ${{ secrets.GITHUB_TOKEN }}"
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Fail workflow if unused keys found
|
||||||
|
if: env.unused_keys != '[]'
|
||||||
|
run: exit 1 # This makes the PR fail if unused keys exist
|
||||||
72
.github/workflows/locize-i18n-sync.yml
vendored
Normal file
72
.github/workflows/locize-i18n-sync.yml
vendored
Normal file
|
|
@ -0,0 +1,72 @@
|
||||||
|
name: Sync Locize Translations & Create Translation PR
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
repository_dispatch:
|
||||||
|
types: [locize/versionPublished]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
sync-translations:
|
||||||
|
name: Sync Translation Keys with Locize
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout Repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set Up Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 20
|
||||||
|
|
||||||
|
- name: Install locize CLI
|
||||||
|
run: npm install -g locize-cli
|
||||||
|
|
||||||
|
# Sync translations (Push missing keys & remove deleted ones)
|
||||||
|
- name: Sync Locize with Repository
|
||||||
|
if: ${{ github.event_name == 'push' }}
|
||||||
|
run: |
|
||||||
|
cd client/src/locales
|
||||||
|
locize sync --api-key ${{ secrets.LOCIZE_API_KEY }} --project-id ${{ secrets.LOCIZE_PROJECT_ID }} --language en
|
||||||
|
|
||||||
|
# When triggered by repository_dispatch, skip sync step.
|
||||||
|
- name: Skip sync step on non-push events
|
||||||
|
if: ${{ github.event_name != 'push' }}
|
||||||
|
run: echo "Skipping sync as the event is not a push."
|
||||||
|
|
||||||
|
create-pull-request:
|
||||||
|
name: Create Translation PR on Version Published
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: sync-translations
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
pull-requests: write
|
||||||
|
steps:
|
||||||
|
# 1. Check out the repository.
|
||||||
|
- name: Checkout Repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
# 2. Download translation files from locize.
|
||||||
|
- name: Download Translations from locize
|
||||||
|
uses: locize/download@v1
|
||||||
|
with:
|
||||||
|
project-id: ${{ secrets.LOCIZE_PROJECT_ID }}
|
||||||
|
path: "client/src/locales"
|
||||||
|
|
||||||
|
# 3. Create a Pull Request using built-in functionality.
|
||||||
|
- name: Create Pull Request
|
||||||
|
uses: peter-evans/create-pull-request@v7
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
sign-commits: true
|
||||||
|
commit-message: "🌍 i18n: Update translation.json with latest translations"
|
||||||
|
base: main
|
||||||
|
branch: i18n/locize-translation-update
|
||||||
|
reviewers: danny-avila
|
||||||
|
title: "🌍 i18n: Update translation.json with latest translations"
|
||||||
|
body: |
|
||||||
|
**Description**:
|
||||||
|
- 🎯 **Objective**: Update `translation.json` with the latest translations from locize.
|
||||||
|
- 🔍 **Details**: This PR is automatically generated upon receiving a versionPublished event with version "latest". It reflects the newest translations provided by locize.
|
||||||
|
- ✅ **Status**: Ready for review.
|
||||||
|
labels: "🌍 i18n"
|
||||||
147
.github/workflows/unused-packages.yml
vendored
Normal file
147
.github/workflows/unused-packages.yml
vendored
Normal file
|
|
@ -0,0 +1,147 @@
|
||||||
|
name: Detect Unused NPM Packages
|
||||||
|
|
||||||
|
on: [pull_request]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
detect-unused-packages:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
pull-requests: write
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Use Node.js 20.x
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 20
|
||||||
|
cache: 'npm'
|
||||||
|
|
||||||
|
- name: Install depcheck
|
||||||
|
run: npm install -g depcheck
|
||||||
|
|
||||||
|
- name: Validate JSON files
|
||||||
|
run: |
|
||||||
|
for FILE in package.json client/package.json api/package.json; do
|
||||||
|
if [[ -f "$FILE" ]]; then
|
||||||
|
jq empty "$FILE" || (echo "::error title=Invalid JSON::$FILE is invalid" && exit 1)
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
- name: Extract Dependencies Used in Scripts
|
||||||
|
id: extract-used-scripts
|
||||||
|
run: |
|
||||||
|
extract_deps_from_scripts() {
|
||||||
|
local package_file=$1
|
||||||
|
if [[ -f "$package_file" ]]; then
|
||||||
|
jq -r '.scripts | to_entries[].value' "$package_file" | \
|
||||||
|
grep -oE '([a-zA-Z0-9_-]+)' | sort -u > used_scripts.txt
|
||||||
|
else
|
||||||
|
touch used_scripts.txt
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
extract_deps_from_scripts "package.json"
|
||||||
|
mv used_scripts.txt root_used_deps.txt
|
||||||
|
|
||||||
|
extract_deps_from_scripts "client/package.json"
|
||||||
|
mv used_scripts.txt client_used_deps.txt
|
||||||
|
|
||||||
|
extract_deps_from_scripts "api/package.json"
|
||||||
|
mv used_scripts.txt api_used_deps.txt
|
||||||
|
|
||||||
|
- name: Extract Dependencies Used in Source Code
|
||||||
|
id: extract-used-code
|
||||||
|
run: |
|
||||||
|
extract_deps_from_code() {
|
||||||
|
local folder=$1
|
||||||
|
local output_file=$2
|
||||||
|
if [[ -d "$folder" ]]; then
|
||||||
|
grep -rEho "require\\(['\"]([a-zA-Z0-9@/._-]+)['\"]\\)" "$folder" --include=\*.{js,ts,mjs,cjs} | \
|
||||||
|
sed -E "s/require\\(['\"]([a-zA-Z0-9@/._-]+)['\"]\\)/\1/" > "$output_file"
|
||||||
|
|
||||||
|
grep -rEho "import .* from ['\"]([a-zA-Z0-9@/._-]+)['\"]" "$folder" --include=\*.{js,ts,mjs,cjs} | \
|
||||||
|
sed -E "s/import .* from ['\"]([a-zA-Z0-9@/._-]+)['\"]/\1/" >> "$output_file"
|
||||||
|
|
||||||
|
sort -u "$output_file" -o "$output_file"
|
||||||
|
else
|
||||||
|
touch "$output_file"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
extract_deps_from_code "." root_used_code.txt
|
||||||
|
extract_deps_from_code "client" client_used_code.txt
|
||||||
|
extract_deps_from_code "api" api_used_code.txt
|
||||||
|
|
||||||
|
- name: Run depcheck for root package.json
|
||||||
|
id: check-root
|
||||||
|
run: |
|
||||||
|
if [[ -f "package.json" ]]; then
|
||||||
|
UNUSED=$(depcheck --json | jq -r '.dependencies | join("\n")' || echo "")
|
||||||
|
UNUSED=$(comm -23 <(echo "$UNUSED" | sort) <(cat root_used_deps.txt root_used_code.txt | sort) || echo "")
|
||||||
|
echo "ROOT_UNUSED<<EOF" >> $GITHUB_ENV
|
||||||
|
echo "$UNUSED" >> $GITHUB_ENV
|
||||||
|
echo "EOF" >> $GITHUB_ENV
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Run depcheck for client/package.json
|
||||||
|
id: check-client
|
||||||
|
run: |
|
||||||
|
if [[ -f "client/package.json" ]]; then
|
||||||
|
chmod -R 755 client
|
||||||
|
cd client
|
||||||
|
UNUSED=$(depcheck --json | jq -r '.dependencies | join("\n")' || echo "")
|
||||||
|
UNUSED=$(comm -23 <(echo "$UNUSED" | sort) <(cat ../client_used_deps.txt ../client_used_code.txt | sort) || echo "")
|
||||||
|
echo "CLIENT_UNUSED<<EOF" >> $GITHUB_ENV
|
||||||
|
echo "$UNUSED" >> $GITHUB_ENV
|
||||||
|
echo "EOF" >> $GITHUB_ENV
|
||||||
|
cd ..
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Run depcheck for api/package.json
|
||||||
|
id: check-api
|
||||||
|
run: |
|
||||||
|
if [[ -f "api/package.json" ]]; then
|
||||||
|
chmod -R 755 api
|
||||||
|
cd api
|
||||||
|
UNUSED=$(depcheck --json | jq -r '.dependencies | join("\n")' || echo "")
|
||||||
|
UNUSED=$(comm -23 <(echo "$UNUSED" | sort) <(cat ../api_used_deps.txt ../api_used_code.txt | sort) || echo "")
|
||||||
|
echo "API_UNUSED<<EOF" >> $GITHUB_ENV
|
||||||
|
echo "$UNUSED" >> $GITHUB_ENV
|
||||||
|
echo "EOF" >> $GITHUB_ENV
|
||||||
|
cd ..
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Post comment on PR if unused dependencies are found
|
||||||
|
if: env.ROOT_UNUSED != '' || env.CLIENT_UNUSED != '' || env.API_UNUSED != ''
|
||||||
|
run: |
|
||||||
|
PR_NUMBER=$(jq --raw-output .pull_request.number "$GITHUB_EVENT_PATH")
|
||||||
|
|
||||||
|
ROOT_LIST=$(echo "$ROOT_UNUSED" | awk '{print "- `" $0 "`"}')
|
||||||
|
CLIENT_LIST=$(echo "$CLIENT_UNUSED" | awk '{print "- `" $0 "`"}')
|
||||||
|
API_LIST=$(echo "$API_UNUSED" | awk '{print "- `" $0 "`"}')
|
||||||
|
|
||||||
|
COMMENT_BODY=$(cat <<EOF
|
||||||
|
### 🚨 Unused NPM Packages Detected
|
||||||
|
|
||||||
|
The following **unused dependencies** were found:
|
||||||
|
|
||||||
|
$(if [[ ! -z "$ROOT_UNUSED" ]]; then echo "#### 📂 Root \`package.json\`"; echo ""; echo "$ROOT_LIST"; echo ""; fi)
|
||||||
|
|
||||||
|
$(if [[ ! -z "$CLIENT_UNUSED" ]]; then echo "#### 📂 Client \`client/package.json\`"; echo ""; echo "$CLIENT_LIST"; echo ""; fi)
|
||||||
|
|
||||||
|
$(if [[ ! -z "$API_UNUSED" ]]; then echo "#### 📂 API \`api/package.json\`"; echo ""; echo "$API_LIST"; echo ""; fi)
|
||||||
|
|
||||||
|
⚠️ **Please remove these unused dependencies to keep your project clean.**
|
||||||
|
EOF
|
||||||
|
)
|
||||||
|
|
||||||
|
gh api "repos/${{ github.repository }}/issues/${PR_NUMBER}/comments" \
|
||||||
|
-f body="$COMMENT_BODY" \
|
||||||
|
-H "Authorization: token ${{ secrets.GITHUB_TOKEN }}"
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Fail workflow if unused dependencies found
|
||||||
|
if: env.ROOT_UNUSED != '' || env.CLIENT_UNUSED != '' || env.API_UNUSED != ''
|
||||||
|
run: exit 1
|
||||||
19
.prettierrc
Normal file
19
.prettierrc
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
{
|
||||||
|
"tailwindConfig": "./client/tailwind.config.cjs",
|
||||||
|
"printWidth": 100,
|
||||||
|
"tabWidth": 2,
|
||||||
|
"useTabs": false,
|
||||||
|
"semi": true,
|
||||||
|
"singleQuote": true,
|
||||||
|
"trailingComma": "all",
|
||||||
|
"arrowParens": "always",
|
||||||
|
"embeddedLanguageFormatting": "auto",
|
||||||
|
"insertPragma": false,
|
||||||
|
"proseWrap": "preserve",
|
||||||
|
"quoteProps": "as-needed",
|
||||||
|
"requirePragma": false,
|
||||||
|
"rangeStart": 0,
|
||||||
|
"endOfLine": "auto",
|
||||||
|
"jsxSingleQuote": false,
|
||||||
|
"plugins": ["prettier-plugin-tailwindcss"]
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
# v0.7.6
|
# v0.7.7-rc1
|
||||||
|
|
||||||
# Base node image
|
# Base node image
|
||||||
FROM node:20-alpine AS node
|
FROM node:20-alpine AS node
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
# Dockerfile.multi
|
# Dockerfile.multi
|
||||||
# v0.7.6
|
# v0.7.7-rc1
|
||||||
|
|
||||||
# Base for all builds
|
# Base for all builds
|
||||||
FROM node:20-alpine AS base-min
|
FROM node:20-alpine AS base-min
|
||||||
|
|
|
||||||
26
README.md
26
README.md
|
|
@ -38,6 +38,15 @@
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<a href="https://www.librechat.ai/docs/translation">
|
||||||
|
<img
|
||||||
|
src="https://img.shields.io/badge/dynamic/json.svg?style=for-the-badge&color=2096F3&label=locize&query=%24.translatedPercentage&url=https://api.locize.app/badgedata/4cb2598b-ed4d-469c-9b04-2ed531a8cb45&suffix=%+translated"
|
||||||
|
alt="Translation Progress">
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
|
||||||
# ✨ Features
|
# ✨ Features
|
||||||
|
|
||||||
- 🖥️ **UI & Experience** inspired by ChatGPT with enhanced design and features
|
- 🖥️ **UI & Experience** inspired by ChatGPT with enhanced design and features
|
||||||
|
|
@ -79,6 +88,9 @@
|
||||||
- English, 中文, Deutsch, Español, Français, Italiano, Polski, Português Brasileiro
|
- English, 中文, Deutsch, Español, Français, Italiano, Polski, Português Brasileiro
|
||||||
- Русский, 日本語, Svenska, 한국어, Tiếng Việt, 繁體中文, العربية, Türkçe, Nederlands, עברית
|
- Русский, 日本語, Svenska, 한국어, Tiếng Việt, 繁體中文, العربية, Türkçe, Nederlands, עברית
|
||||||
|
|
||||||
|
- 🧠 **Reasoning UI**:
|
||||||
|
- Dynamic Reasoning UI for Chain-of-Thought/Reasoning AI models like DeepSeek-R1
|
||||||
|
|
||||||
- 🎨 **Customizable Interface**:
|
- 🎨 **Customizable Interface**:
|
||||||
- Customizable Dropdown & Interface that adapts to both power users and newcomers
|
- Customizable Dropdown & Interface that adapts to both power users and newcomers
|
||||||
|
|
||||||
|
|
@ -167,6 +179,8 @@ Contributions, suggestions, bug reports and fixes are welcome!
|
||||||
|
|
||||||
For new features, components, or extensions, please open an issue and discuss before sending a PR.
|
For new features, components, or extensions, please open an issue and discuss before sending a PR.
|
||||||
|
|
||||||
|
If you'd like to help translate LibreChat into your language, we'd love your contribution! Improving our translations not only makes LibreChat more accessible to users around the world but also enhances the overall user experience. Please check out our [Translation Guide](https://www.librechat.ai/docs/translation).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 💖 This project exists in its current state thanks to all the people who contribute
|
## 💖 This project exists in its current state thanks to all the people who contribute
|
||||||
|
|
@ -174,3 +188,15 @@ For new features, components, or extensions, please open an issue and discuss be
|
||||||
<a href="https://github.com/danny-avila/LibreChat/graphs/contributors">
|
<a href="https://github.com/danny-avila/LibreChat/graphs/contributors">
|
||||||
<img src="https://contrib.rocks/image?repo=danny-avila/LibreChat" />
|
<img src="https://contrib.rocks/image?repo=danny-avila/LibreChat" />
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎉 Special Thanks
|
||||||
|
|
||||||
|
We thank [Locize](https://locize.com) for their translation management tools that support multiple languages in LibreChat.
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<a href="https://locize.com" target="_blank" rel="noopener noreferrer">
|
||||||
|
<img src="https://locize.com/img/locize_color.svg" alt="Locize Logo" height="50">
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
|
|
||||||
|
|
@ -506,9 +506,8 @@ class OpenAIClient extends BaseClient {
|
||||||
if (promptPrefix && this.isOmni === true) {
|
if (promptPrefix && this.isOmni === true) {
|
||||||
const lastUserMessageIndex = payload.findLastIndex((message) => message.role === 'user');
|
const lastUserMessageIndex = payload.findLastIndex((message) => message.role === 'user');
|
||||||
if (lastUserMessageIndex !== -1) {
|
if (lastUserMessageIndex !== -1) {
|
||||||
payload[
|
payload[lastUserMessageIndex].content =
|
||||||
lastUserMessageIndex
|
`${promptPrefix}\n${payload[lastUserMessageIndex].content}`;
|
||||||
].content = `${promptPrefix}\n${payload[lastUserMessageIndex].content}`;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1067,14 +1066,36 @@ ${convo}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
getStreamText() {
|
/**
|
||||||
|
*
|
||||||
|
* @param {string[]} [intermediateReply]
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
getStreamText(intermediateReply) {
|
||||||
if (!this.streamHandler) {
|
if (!this.streamHandler) {
|
||||||
return '';
|
return intermediateReply?.join('') ?? '';
|
||||||
|
}
|
||||||
|
|
||||||
|
let thinkMatch;
|
||||||
|
let remainingText;
|
||||||
|
let reasoningText = '';
|
||||||
|
|
||||||
|
if (this.streamHandler.reasoningTokens.length > 0) {
|
||||||
|
reasoningText = this.streamHandler.reasoningTokens.join('');
|
||||||
|
thinkMatch = reasoningText.match(/<think>([\s\S]*?)<\/think>/)?.[1]?.trim();
|
||||||
|
if (thinkMatch != null && thinkMatch) {
|
||||||
|
const reasoningTokens = `:::thinking\n${thinkMatch}\n:::\n`;
|
||||||
|
remainingText = reasoningText.split(/<\/think>/)?.[1]?.trim() || '';
|
||||||
|
return `${reasoningTokens}${remainingText}${this.streamHandler.tokens.join('')}`;
|
||||||
|
} else if (thinkMatch === '') {
|
||||||
|
remainingText = reasoningText.split(/<\/think>/)?.[1]?.trim() || '';
|
||||||
|
return `${remainingText}${this.streamHandler.tokens.join('')}`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const reasoningTokens =
|
const reasoningTokens =
|
||||||
this.streamHandler.reasoningTokens.length > 0
|
reasoningText.length > 0
|
||||||
? `:::thinking\n${this.streamHandler.reasoningTokens.join('')}\n:::\n`
|
? `:::thinking\n${reasoningText.replace('<think>', '').replace('</think>', '').trim()}\n:::\n`
|
||||||
: '';
|
: '';
|
||||||
|
|
||||||
return `${reasoningTokens}${this.streamHandler.tokens.join('')}`;
|
return `${reasoningTokens}${this.streamHandler.tokens.join('')}`;
|
||||||
|
|
@ -1314,11 +1335,19 @@ ${convo}
|
||||||
streamPromise = new Promise((resolve) => {
|
streamPromise = new Promise((resolve) => {
|
||||||
streamResolve = resolve;
|
streamResolve = resolve;
|
||||||
});
|
});
|
||||||
|
/** @type {OpenAI.OpenAI.CompletionCreateParamsStreaming} */
|
||||||
|
const params = {
|
||||||
|
...modelOptions,
|
||||||
|
stream: true,
|
||||||
|
};
|
||||||
|
if (
|
||||||
|
this.options.endpoint === EModelEndpoint.openAI ||
|
||||||
|
this.options.endpoint === EModelEndpoint.azureOpenAI
|
||||||
|
) {
|
||||||
|
params.stream_options = { include_usage: true };
|
||||||
|
}
|
||||||
const stream = await openai.beta.chat.completions
|
const stream = await openai.beta.chat.completions
|
||||||
.stream({
|
.stream(params)
|
||||||
...modelOptions,
|
|
||||||
stream: true,
|
|
||||||
})
|
|
||||||
.on('abort', () => {
|
.on('abort', () => {
|
||||||
/* Do nothing here */
|
/* Do nothing here */
|
||||||
})
|
})
|
||||||
|
|
@ -1449,7 +1478,7 @@ ${convo}
|
||||||
this.options.context !== 'title' &&
|
this.options.context !== 'title' &&
|
||||||
message.content.startsWith('<think>')
|
message.content.startsWith('<think>')
|
||||||
) {
|
) {
|
||||||
return message.content.replace('<think>', ':::thinking').replace('</think>', ':::');
|
return this.getStreamText();
|
||||||
}
|
}
|
||||||
|
|
||||||
return message.content;
|
return message.content;
|
||||||
|
|
@ -1458,7 +1487,7 @@ ${convo}
|
||||||
err?.message?.includes('abort') ||
|
err?.message?.includes('abort') ||
|
||||||
(err instanceof OpenAI.APIError && err?.message?.includes('abort'))
|
(err instanceof OpenAI.APIError && err?.message?.includes('abort'))
|
||||||
) {
|
) {
|
||||||
return intermediateReply.join('');
|
return this.getStreamText(intermediateReply);
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
err?.message?.includes(
|
err?.message?.includes(
|
||||||
|
|
@ -1473,14 +1502,18 @@ ${convo}
|
||||||
(err instanceof OpenAI.OpenAIError && err?.message?.includes('missing finish_reason'))
|
(err instanceof OpenAI.OpenAIError && err?.message?.includes('missing finish_reason'))
|
||||||
) {
|
) {
|
||||||
logger.error('[OpenAIClient] Known OpenAI error:', err);
|
logger.error('[OpenAIClient] Known OpenAI error:', err);
|
||||||
if (intermediateReply.length > 0) {
|
if (this.streamHandler && this.streamHandler.reasoningTokens.length) {
|
||||||
return intermediateReply.join('');
|
return this.getStreamText();
|
||||||
|
} else if (intermediateReply.length > 0) {
|
||||||
|
return this.getStreamText(intermediateReply);
|
||||||
} else {
|
} else {
|
||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
} else if (err instanceof OpenAI.APIError) {
|
} else if (err instanceof OpenAI.APIError) {
|
||||||
if (intermediateReply.length > 0) {
|
if (this.streamHandler && this.streamHandler.reasoningTokens.length) {
|
||||||
return intermediateReply.join('');
|
return this.getStreamText();
|
||||||
|
} else if (intermediateReply.length > 0) {
|
||||||
|
return this.getStreamText(intermediateReply);
|
||||||
} else {
|
} else {
|
||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
5
api/cache/getLogStores.js
vendored
5
api/cache/getLogStores.js
vendored
|
|
@ -37,6 +37,10 @@ const messages = isRedisEnabled
|
||||||
? new Keyv({ store: keyvRedis, ttl: Time.ONE_MINUTE })
|
? new Keyv({ store: keyvRedis, ttl: Time.ONE_MINUTE })
|
||||||
: new Keyv({ namespace: CacheKeys.MESSAGES, ttl: Time.ONE_MINUTE });
|
: new Keyv({ namespace: CacheKeys.MESSAGES, ttl: Time.ONE_MINUTE });
|
||||||
|
|
||||||
|
const flows = isRedisEnabled
|
||||||
|
? new Keyv({ store: keyvRedis, ttl: Time.TWO_MINUTES })
|
||||||
|
: new Keyv({ namespace: CacheKeys.FLOWS, ttl: Time.ONE_MINUTE * 3 });
|
||||||
|
|
||||||
const tokenConfig = isRedisEnabled
|
const tokenConfig = isRedisEnabled
|
||||||
? new Keyv({ store: keyvRedis, ttl: Time.THIRTY_MINUTES })
|
? new Keyv({ store: keyvRedis, ttl: Time.THIRTY_MINUTES })
|
||||||
: new Keyv({ namespace: CacheKeys.TOKEN_CONFIG, ttl: Time.THIRTY_MINUTES });
|
: new Keyv({ namespace: CacheKeys.TOKEN_CONFIG, ttl: Time.THIRTY_MINUTES });
|
||||||
|
|
@ -88,6 +92,7 @@ const namespaces = {
|
||||||
[CacheKeys.MODEL_QUERIES]: modelQueries,
|
[CacheKeys.MODEL_QUERIES]: modelQueries,
|
||||||
[CacheKeys.AUDIO_RUNS]: audioRuns,
|
[CacheKeys.AUDIO_RUNS]: audioRuns,
|
||||||
[CacheKeys.MESSAGES]: messages,
|
[CacheKeys.MESSAGES]: messages,
|
||||||
|
[CacheKeys.FLOWS]: flows,
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
2
api/cache/keyvRedis.js
vendored
2
api/cache/keyvRedis.js
vendored
|
|
@ -1,6 +1,6 @@
|
||||||
const KeyvRedis = require('@keyv/redis');
|
const KeyvRedis = require('@keyv/redis');
|
||||||
const { logger } = require('~/config');
|
|
||||||
const { isEnabled } = require('~/server/utils');
|
const { isEnabled } = require('~/server/utils');
|
||||||
|
const logger = require('~/config/winston');
|
||||||
|
|
||||||
const { REDIS_URI, USE_REDIS } = process.env;
|
const { REDIS_URI, USE_REDIS } = process.env;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,11 @@
|
||||||
const { EventSource } = require('eventsource');
|
const { EventSource } = require('eventsource');
|
||||||
|
const { Time, CacheKeys } = require('librechat-data-provider');
|
||||||
const logger = require('./winston');
|
const logger = require('./winston');
|
||||||
|
|
||||||
global.EventSource = EventSource;
|
global.EventSource = EventSource;
|
||||||
|
|
||||||
let mcpManager = null;
|
let mcpManager = null;
|
||||||
|
let flowManager = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @returns {Promise<MCPManager>}
|
* @returns {Promise<MCPManager>}
|
||||||
|
|
@ -16,6 +18,21 @@ async function getMCPManager() {
|
||||||
return mcpManager;
|
return mcpManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {(key: string) => Keyv} getLogStores
|
||||||
|
* @returns {Promise<FlowStateManager>}
|
||||||
|
*/
|
||||||
|
async function getFlowStateManager(getLogStores) {
|
||||||
|
if (!flowManager) {
|
||||||
|
const { FlowStateManager } = await import('librechat-mcp');
|
||||||
|
flowManager = new FlowStateManager(getLogStores(CacheKeys.FLOWS), {
|
||||||
|
ttl: Time.ONE_MINUTE * 3,
|
||||||
|
logger,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return flowManager;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sends message data in Server Sent Events format.
|
* Sends message data in Server Sent Events format.
|
||||||
* @param {ServerResponse} res - The server response.
|
* @param {ServerResponse} res - The server response.
|
||||||
|
|
@ -34,4 +51,5 @@ module.exports = {
|
||||||
logger,
|
logger,
|
||||||
sendEvent,
|
sendEvent,
|
||||||
getMCPManager,
|
getMCPManager,
|
||||||
|
getFlowStateManager,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
const tokenSchema = require('./schema/tokenSchema');
|
|
||||||
const mongoose = require('mongoose');
|
const mongoose = require('mongoose');
|
||||||
|
const { encryptV2 } = require('~/server/utils/crypto');
|
||||||
|
const tokenSchema = require('./schema/tokenSchema');
|
||||||
const { logger } = require('~/config');
|
const { logger } = require('~/config');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -7,6 +8,32 @@ const { logger } = require('~/config');
|
||||||
* @type {mongoose.Model}
|
* @type {mongoose.Model}
|
||||||
*/
|
*/
|
||||||
const Token = mongoose.model('Token', tokenSchema);
|
const Token = mongoose.model('Token', tokenSchema);
|
||||||
|
/**
|
||||||
|
* Fixes the indexes for the Token collection from legacy TTL indexes to the new expiresAt index.
|
||||||
|
*/
|
||||||
|
async function fixIndexes() {
|
||||||
|
try {
|
||||||
|
const indexes = await Token.collection.indexes();
|
||||||
|
logger.debug('Existing Token Indexes:', JSON.stringify(indexes, null, 2));
|
||||||
|
const unwantedTTLIndexes = indexes.filter(
|
||||||
|
(index) => index.key.createdAt === 1 && index.expireAfterSeconds !== undefined,
|
||||||
|
);
|
||||||
|
if (unwantedTTLIndexes.length === 0) {
|
||||||
|
logger.debug('No unwanted Token indexes found.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (const index of unwantedTTLIndexes) {
|
||||||
|
logger.debug(`Dropping unwanted Token index: ${index.name}`);
|
||||||
|
await Token.collection.dropIndex(index.name);
|
||||||
|
logger.debug(`Dropped Token index: ${index.name}`);
|
||||||
|
}
|
||||||
|
logger.debug('Token index cleanup completed successfully.');
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('An error occurred while fixing Token indexes:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fixIndexes();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new Token instance.
|
* Creates a new Token instance.
|
||||||
|
|
@ -29,8 +56,7 @@ async function createToken(tokenData) {
|
||||||
expiresAt,
|
expiresAt,
|
||||||
};
|
};
|
||||||
|
|
||||||
const newToken = new Token(newTokenData);
|
return await Token.create(newTokenData);
|
||||||
return await newToken.save();
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.debug('An error occurred while creating token:', error);
|
logger.debug('An error occurred while creating token:', error);
|
||||||
throw error;
|
throw error;
|
||||||
|
|
@ -42,7 +68,8 @@ async function createToken(tokenData) {
|
||||||
* @param {Object} query - The query to match against.
|
* @param {Object} query - The query to match against.
|
||||||
* @param {mongoose.Types.ObjectId|String} query.userId - The ID of the user.
|
* @param {mongoose.Types.ObjectId|String} query.userId - The ID of the user.
|
||||||
* @param {String} query.token - The token value.
|
* @param {String} query.token - The token value.
|
||||||
* @param {String} query.email - The email of the user.
|
* @param {String} [query.email] - The email of the user.
|
||||||
|
* @param {String} [query.identifier] - Unique, alternative identifier for the token.
|
||||||
* @returns {Promise<Object|null>} The matched Token document, or null if not found.
|
* @returns {Promise<Object|null>} The matched Token document, or null if not found.
|
||||||
* @throws Will throw an error if the find operation fails.
|
* @throws Will throw an error if the find operation fails.
|
||||||
*/
|
*/
|
||||||
|
|
@ -59,6 +86,9 @@ async function findToken(query) {
|
||||||
if (query.email) {
|
if (query.email) {
|
||||||
conditions.push({ email: query.email });
|
conditions.push({ email: query.email });
|
||||||
}
|
}
|
||||||
|
if (query.identifier) {
|
||||||
|
conditions.push({ identifier: query.identifier });
|
||||||
|
}
|
||||||
|
|
||||||
const token = await Token.findOne({
|
const token = await Token.findOne({
|
||||||
$and: conditions,
|
$and: conditions,
|
||||||
|
|
@ -76,6 +106,8 @@ async function findToken(query) {
|
||||||
* @param {Object} query - The query to match against.
|
* @param {Object} query - The query to match against.
|
||||||
* @param {mongoose.Types.ObjectId|String} query.userId - The ID of the user.
|
* @param {mongoose.Types.ObjectId|String} query.userId - The ID of the user.
|
||||||
* @param {String} query.token - The token value.
|
* @param {String} query.token - The token value.
|
||||||
|
* @param {String} [query.email] - The email of the user.
|
||||||
|
* @param {String} [query.identifier] - Unique, alternative identifier for the token.
|
||||||
* @param {Object} updateData - The data to update the Token with.
|
* @param {Object} updateData - The data to update the Token with.
|
||||||
* @returns {Promise<mongoose.Document|null>} The updated Token document, or null if not found.
|
* @returns {Promise<mongoose.Document|null>} The updated Token document, or null if not found.
|
||||||
* @throws Will throw an error if the update operation fails.
|
* @throws Will throw an error if the update operation fails.
|
||||||
|
|
@ -94,14 +126,20 @@ async function updateToken(query, updateData) {
|
||||||
* @param {Object} query - The query to match against.
|
* @param {Object} query - The query to match against.
|
||||||
* @param {mongoose.Types.ObjectId|String} query.userId - The ID of the user.
|
* @param {mongoose.Types.ObjectId|String} query.userId - The ID of the user.
|
||||||
* @param {String} query.token - The token value.
|
* @param {String} query.token - The token value.
|
||||||
* @param {String} query.email - The email of the user.
|
* @param {String} [query.email] - The email of the user.
|
||||||
|
* @param {String} [query.identifier] - Unique, alternative identifier for the token.
|
||||||
* @returns {Promise<Object>} The result of the delete operation.
|
* @returns {Promise<Object>} The result of the delete operation.
|
||||||
* @throws Will throw an error if the delete operation fails.
|
* @throws Will throw an error if the delete operation fails.
|
||||||
*/
|
*/
|
||||||
async function deleteTokens(query) {
|
async function deleteTokens(query) {
|
||||||
try {
|
try {
|
||||||
return await Token.deleteMany({
|
return await Token.deleteMany({
|
||||||
$or: [{ userId: query.userId }, { token: query.token }, { email: query.email }],
|
$or: [
|
||||||
|
{ userId: query.userId },
|
||||||
|
{ token: query.token },
|
||||||
|
{ email: query.email },
|
||||||
|
{ identifier: query.identifier },
|
||||||
|
],
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.debug('An error occurred while deleting tokens:', error);
|
logger.debug('An error occurred while deleting tokens:', error);
|
||||||
|
|
@ -109,9 +147,46 @@ async function deleteTokens(query) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles the OAuth token by creating or updating the token.
|
||||||
|
* @param {object} fields
|
||||||
|
* @param {string} fields.userId - The user's ID.
|
||||||
|
* @param {string} fields.token - The full token to store.
|
||||||
|
* @param {string} fields.identifier - Unique, alternative identifier for the token.
|
||||||
|
* @param {number} fields.expiresIn - The number of seconds until the token expires.
|
||||||
|
* @param {object} fields.metadata - Additional metadata to store with the token.
|
||||||
|
* @param {string} [fields.type="oauth"] - The type of token. Default is 'oauth'.
|
||||||
|
*/
|
||||||
|
async function handleOAuthToken({
|
||||||
|
token,
|
||||||
|
userId,
|
||||||
|
identifier,
|
||||||
|
expiresIn,
|
||||||
|
metadata,
|
||||||
|
type = 'oauth',
|
||||||
|
}) {
|
||||||
|
const encrypedToken = await encryptV2(token);
|
||||||
|
const tokenData = {
|
||||||
|
type,
|
||||||
|
userId,
|
||||||
|
metadata,
|
||||||
|
identifier,
|
||||||
|
token: encrypedToken,
|
||||||
|
expiresIn: parseInt(expiresIn, 10) || 3600,
|
||||||
|
};
|
||||||
|
|
||||||
|
const existingToken = await findToken({ userId, identifier });
|
||||||
|
if (existingToken) {
|
||||||
|
return await updateToken({ identifier }, tokenData);
|
||||||
|
} else {
|
||||||
|
return await createToken(tokenData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
createToken,
|
|
||||||
findToken,
|
findToken,
|
||||||
|
createToken,
|
||||||
updateToken,
|
updateToken,
|
||||||
deleteTokens,
|
deleteTokens,
|
||||||
|
handleOAuthToken,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,9 @@ const agentSchema = mongoose.Schema(
|
||||||
model_parameters: {
|
model_parameters: {
|
||||||
type: Object,
|
type: Object,
|
||||||
},
|
},
|
||||||
|
artifacts: {
|
||||||
|
type: String,
|
||||||
|
},
|
||||||
access_level: {
|
access_level: {
|
||||||
type: Number,
|
type: Number,
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,10 @@ const tokenSchema = new Schema({
|
||||||
email: {
|
email: {
|
||||||
type: String,
|
type: String,
|
||||||
},
|
},
|
||||||
|
type: String,
|
||||||
|
identifier: {
|
||||||
|
type: String,
|
||||||
|
},
|
||||||
token: {
|
token: {
|
||||||
type: String,
|
type: String,
|
||||||
required: true,
|
required: true,
|
||||||
|
|
@ -23,6 +27,10 @@ const tokenSchema = new Schema({
|
||||||
type: Date,
|
type: Date,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
|
metadata: {
|
||||||
|
type: Map,
|
||||||
|
of: Schema.Types.Mixed,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
tokenSchema.index({ expiresAt: 1 }, { expireAfterSeconds: 0 });
|
tokenSchema.index({ expiresAt: 1 }, { expireAfterSeconds: 0 });
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@librechat/backend",
|
"name": "@librechat/backend",
|
||||||
"version": "v0.7.6",
|
"version": "v0.7.7-rc1",
|
||||||
"description": "",
|
"description": "",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "echo 'please run this from the root directory'",
|
"start": "echo 'please run this from the root directory'",
|
||||||
|
|
@ -45,11 +45,10 @@
|
||||||
"@langchain/google-genai": "^0.1.7",
|
"@langchain/google-genai": "^0.1.7",
|
||||||
"@langchain/google-vertexai": "^0.1.8",
|
"@langchain/google-vertexai": "^0.1.8",
|
||||||
"@langchain/textsplitters": "^0.1.0",
|
"@langchain/textsplitters": "^0.1.0",
|
||||||
"@librechat/agents": "^2.0.2",
|
"@librechat/agents": "^2.0.4",
|
||||||
"@waylaidwanderer/fetch-event-source": "^3.0.1",
|
"@waylaidwanderer/fetch-event-source": "^3.0.1",
|
||||||
"axios": "^1.7.7",
|
"axios": "1.7.8",
|
||||||
"bcryptjs": "^2.4.3",
|
"bcryptjs": "^2.4.3",
|
||||||
"cheerio": "^1.0.0-rc.12",
|
|
||||||
"cohere-ai": "^7.9.1",
|
"cohere-ai": "^7.9.1",
|
||||||
"compression": "^1.7.4",
|
"compression": "^1.7.4",
|
||||||
"connect-redis": "^7.1.0",
|
"connect-redis": "^7.1.0",
|
||||||
|
|
@ -66,7 +65,6 @@
|
||||||
"firebase": "^11.0.2",
|
"firebase": "^11.0.2",
|
||||||
"googleapis": "^126.0.1",
|
"googleapis": "^126.0.1",
|
||||||
"handlebars": "^4.7.7",
|
"handlebars": "^4.7.7",
|
||||||
"html": "^1.0.0",
|
|
||||||
"ioredis": "^5.3.2",
|
"ioredis": "^5.3.2",
|
||||||
"js-yaml": "^4.1.0",
|
"js-yaml": "^4.1.0",
|
||||||
"jsonwebtoken": "^9.0.0",
|
"jsonwebtoken": "^9.0.0",
|
||||||
|
|
@ -91,7 +89,6 @@
|
||||||
"openid-client": "^5.4.2",
|
"openid-client": "^5.4.2",
|
||||||
"passport": "^0.6.0",
|
"passport": "^0.6.0",
|
||||||
"passport-apple": "^2.0.2",
|
"passport-apple": "^2.0.2",
|
||||||
"passport-custom": "^1.1.1",
|
|
||||||
"passport-discord": "^0.1.4",
|
"passport-discord": "^0.1.4",
|
||||||
"passport-facebook": "^3.0.0",
|
"passport-facebook": "^3.0.0",
|
||||||
"passport-github2": "^0.1.12",
|
"passport-github2": "^0.1.12",
|
||||||
|
|
@ -99,7 +96,6 @@
|
||||||
"passport-jwt": "^4.0.1",
|
"passport-jwt": "^4.0.1",
|
||||||
"passport-ldapauth": "^3.0.1",
|
"passport-ldapauth": "^3.0.1",
|
||||||
"passport-local": "^1.0.0",
|
"passport-local": "^1.0.0",
|
||||||
"pino": "^8.12.1",
|
|
||||||
"sharp": "^0.32.6",
|
"sharp": "^0.32.6",
|
||||||
"tiktoken": "^1.0.15",
|
"tiktoken": "^1.0.15",
|
||||||
"traverse": "^0.6.7",
|
"traverse": "^0.6.7",
|
||||||
|
|
@ -111,8 +107,8 @@
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"jest": "^29.7.0",
|
"jest": "^29.7.0",
|
||||||
"mongodb-memory-server": "^10.0.0",
|
"mongodb-memory-server": "^10.1.3",
|
||||||
"nodemon": "^3.0.1",
|
"nodemon": "^3.0.3",
|
||||||
"supertest": "^6.3.3"
|
"supertest": "^7.0.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -155,6 +155,8 @@ const AskController = async (req, res, next, initializeClient, addTitle) => {
|
||||||
sender,
|
sender,
|
||||||
messageId: responseMessageId,
|
messageId: responseMessageId,
|
||||||
parentMessageId: userMessageId ?? parentMessageId,
|
parentMessageId: userMessageId ?? parentMessageId,
|
||||||
|
}).catch((err) => {
|
||||||
|
logger.error('[AskController] Error in `handleAbortError`', err);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -140,6 +140,8 @@ const EditController = async (req, res, next, initializeClient) => {
|
||||||
sender,
|
sender,
|
||||||
messageId: responseMessageId,
|
messageId: responseMessageId,
|
||||||
parentMessageId: userMessageId ?? parentMessageId,
|
parentMessageId: userMessageId ?? parentMessageId,
|
||||||
|
}).catch((err) => {
|
||||||
|
logger.error('[EditController] Error in `handleAbortError`', err);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -143,6 +143,8 @@ const AgentController = async (req, res, next, initializeClient, addTitle) => {
|
||||||
sender,
|
sender,
|
||||||
messageId: responseMessageId,
|
messageId: responseMessageId,
|
||||||
parentMessageId: userMessageId ?? parentMessageId,
|
parentMessageId: userMessageId ?? parentMessageId,
|
||||||
|
}).catch((err) => {
|
||||||
|
logger.error('[api/server/controllers/agents/request] Error in `handleAbortError`', err);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -84,6 +84,7 @@ const startServer = async () => {
|
||||||
app.use('/oauth', routes.oauth);
|
app.use('/oauth', routes.oauth);
|
||||||
/* API Endpoints */
|
/* API Endpoints */
|
||||||
app.use('/api/auth', routes.auth);
|
app.use('/api/auth', routes.auth);
|
||||||
|
app.use('/api/actions', routes.actions);
|
||||||
app.use('/api/keys', routes.keys);
|
app.use('/api/keys', routes.keys);
|
||||||
app.use('/api/user', routes.user);
|
app.use('/api/user', routes.user);
|
||||||
app.use('/api/search', routes.search);
|
app.use('/api/search', routes.search);
|
||||||
|
|
|
||||||
136
api/server/routes/actions.js
Normal file
136
api/server/routes/actions.js
Normal file
|
|
@ -0,0 +1,136 @@
|
||||||
|
const express = require('express');
|
||||||
|
const jwt = require('jsonwebtoken');
|
||||||
|
const { getAccessToken } = require('~/server/services/TokenService');
|
||||||
|
const { logger, getFlowStateManager } = require('~/config');
|
||||||
|
const { getLogStores } = require('~/cache');
|
||||||
|
|
||||||
|
const router = express.Router();
|
||||||
|
const JWT_SECRET = process.env.JWT_SECRET;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles the OAuth callback and exchanges the authorization code for tokens.
|
||||||
|
*
|
||||||
|
* @route GET /actions/:action_id/oauth/callback
|
||||||
|
* @param {string} req.params.action_id - The ID of the action.
|
||||||
|
* @param {string} req.query.code - The authorization code returned by the provider.
|
||||||
|
* @param {string} req.query.state - The state token to verify the authenticity of the request.
|
||||||
|
* @returns {void} Sends a success message after updating the action with OAuth tokens.
|
||||||
|
*/
|
||||||
|
router.get('/:action_id/oauth/callback', async (req, res) => {
|
||||||
|
const { action_id } = req.params;
|
||||||
|
const { code, state } = req.query;
|
||||||
|
|
||||||
|
const flowManager = await getFlowStateManager(getLogStores);
|
||||||
|
let identifier = action_id;
|
||||||
|
try {
|
||||||
|
let decodedState;
|
||||||
|
try {
|
||||||
|
decodedState = jwt.verify(state, JWT_SECRET);
|
||||||
|
} catch (err) {
|
||||||
|
await flowManager.failFlow(identifier, 'oauth', 'Invalid or expired state parameter');
|
||||||
|
return res.status(400).send('Invalid or expired state parameter');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (decodedState.action_id !== action_id) {
|
||||||
|
await flowManager.failFlow(identifier, 'oauth', 'Mismatched action ID in state parameter');
|
||||||
|
return res.status(400).send('Mismatched action ID in state parameter');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!decodedState.user) {
|
||||||
|
await flowManager.failFlow(identifier, 'oauth', 'Invalid user ID in state parameter');
|
||||||
|
return res.status(400).send('Invalid user ID in state parameter');
|
||||||
|
}
|
||||||
|
identifier = `${decodedState.user}:${action_id}`;
|
||||||
|
const flowState = await flowManager.getFlowState(identifier, 'oauth');
|
||||||
|
if (!flowState) {
|
||||||
|
throw new Error('OAuth flow not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
const tokenData = await getAccessToken({
|
||||||
|
code,
|
||||||
|
userId: decodedState.user,
|
||||||
|
identifier,
|
||||||
|
client_url: flowState.metadata.client_url,
|
||||||
|
redirect_uri: flowState.metadata.redirect_uri,
|
||||||
|
/** Encrypted values */
|
||||||
|
encrypted_oauth_client_id: flowState.metadata.encrypted_oauth_client_id,
|
||||||
|
encrypted_oauth_client_secret: flowState.metadata.encrypted_oauth_client_secret,
|
||||||
|
});
|
||||||
|
await flowManager.completeFlow(identifier, 'oauth', tokenData);
|
||||||
|
res.send(`
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Authentication Successful</title>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont;
|
||||||
|
background-color: rgb(249, 250, 251);
|
||||||
|
margin: 0;
|
||||||
|
padding: 2rem;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
.card {
|
||||||
|
background-color: white;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
padding: 2rem;
|
||||||
|
max-width: 28rem;
|
||||||
|
width: 100%;
|
||||||
|
box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.heading {
|
||||||
|
color: rgb(17, 24, 39);
|
||||||
|
font-size: 1.875rem;
|
||||||
|
font-weight: 700;
|
||||||
|
margin: 0 0 1rem;
|
||||||
|
}
|
||||||
|
.description {
|
||||||
|
color: rgb(75, 85, 99);
|
||||||
|
font-size: 0.875rem;
|
||||||
|
margin: 0.5rem 0;
|
||||||
|
}
|
||||||
|
.countdown {
|
||||||
|
color: rgb(99, 102, 241);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="card">
|
||||||
|
<h1 class="heading">Authentication Successful</h1>
|
||||||
|
<p class="description">
|
||||||
|
Your authentication was successful. This window will close in
|
||||||
|
<span class="countdown" id="countdown">3</span> seconds.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
let secondsLeft = 3;
|
||||||
|
const countdownElement = document.getElementById('countdown');
|
||||||
|
|
||||||
|
const countdown = setInterval(() => {
|
||||||
|
secondsLeft--;
|
||||||
|
countdownElement.textContent = secondsLeft;
|
||||||
|
|
||||||
|
if (secondsLeft <= 0) {
|
||||||
|
clearInterval(countdown);
|
||||||
|
window.close();
|
||||||
|
}
|
||||||
|
}, 1000);
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Error in OAuth callback:', error);
|
||||||
|
await flowManager.failFlow(identifier, 'oauth', error);
|
||||||
|
res.status(500).send('Authentication failed. Please try again.');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
const express = require('express');
|
const express = require('express');
|
||||||
const { nanoid } = require('nanoid');
|
const { nanoid } = require('nanoid');
|
||||||
const { actionDelimiter, SystemRoles } = require('librechat-data-provider');
|
const { actionDelimiter, SystemRoles, removeNullishValues } = require('librechat-data-provider');
|
||||||
const { encryptMetadata, domainParser } = require('~/server/services/ActionService');
|
const { encryptMetadata, domainParser } = require('~/server/services/ActionService');
|
||||||
const { updateAction, getActions, deleteAction } = require('~/models/Action');
|
const { updateAction, getActions, deleteAction } = require('~/models/Action');
|
||||||
const { isActionDomainAllowed } = require('~/server/services/domains');
|
const { isActionDomainAllowed } = require('~/server/services/domains');
|
||||||
|
|
@ -51,7 +51,7 @@ router.post('/:agent_id', async (req, res) => {
|
||||||
return res.status(400).json({ message: 'No functions provided' });
|
return res.status(400).json({ message: 'No functions provided' });
|
||||||
}
|
}
|
||||||
|
|
||||||
let metadata = await encryptMetadata(_metadata);
|
let metadata = await encryptMetadata(removeNullishValues(_metadata, true));
|
||||||
const isDomainAllowed = await isActionDomainAllowed(metadata.domain);
|
const isDomainAllowed = await isActionDomainAllowed(metadata.domain);
|
||||||
if (!isDomainAllowed) {
|
if (!isDomainAllowed) {
|
||||||
return res.status(400).json({ message: 'Domain not allowed' });
|
return res.status(400).json({ message: 'Domain not allowed' });
|
||||||
|
|
@ -117,10 +117,7 @@ router.post('/:agent_id', async (req, res) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @type {[Action]} */
|
/** @type {[Action]} */
|
||||||
const updatedAction = await updateAction(
|
const updatedAction = await updateAction({ action_id }, actionUpdateData);
|
||||||
{ action_id },
|
|
||||||
actionUpdateData,
|
|
||||||
);
|
|
||||||
|
|
||||||
const sensitiveFields = ['api_key', 'oauth_client_id', 'oauth_client_secret'];
|
const sensitiveFields = ['api_key', 'oauth_client_id', 'oauth_client_secret'];
|
||||||
for (let field of sensitiveFields) {
|
for (let field of sensitiveFields) {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
const express = require('express');
|
const express = require('express');
|
||||||
const { nanoid } = require('nanoid');
|
const { nanoid } = require('nanoid');
|
||||||
const { actionDelimiter, EModelEndpoint } = require('librechat-data-provider');
|
const { actionDelimiter, EModelEndpoint, removeNullishValues } = require('librechat-data-provider');
|
||||||
const { encryptMetadata, domainParser } = require('~/server/services/ActionService');
|
const { encryptMetadata, domainParser } = require('~/server/services/ActionService');
|
||||||
const { getOpenAIClient } = require('~/server/controllers/assistants/helpers');
|
const { getOpenAIClient } = require('~/server/controllers/assistants/helpers');
|
||||||
const { updateAction, getActions, deleteAction } = require('~/models/Action');
|
const { updateAction, getActions, deleteAction } = require('~/models/Action');
|
||||||
|
|
@ -29,7 +29,7 @@ router.post('/:assistant_id', async (req, res) => {
|
||||||
return res.status(400).json({ message: 'No functions provided' });
|
return res.status(400).json({ message: 'No functions provided' });
|
||||||
}
|
}
|
||||||
|
|
||||||
let metadata = await encryptMetadata(_metadata);
|
let metadata = await encryptMetadata(removeNullishValues(_metadata, true));
|
||||||
const isDomainAllowed = await isActionDomainAllowed(metadata.domain);
|
const isDomainAllowed = await isActionDomainAllowed(metadata.domain);
|
||||||
if (!isDomainAllowed) {
|
if (!isDomainAllowed) {
|
||||||
return res.status(400).json({ message: 'Domain not allowed' });
|
return res.status(400).json({ message: 'Domain not allowed' });
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ const prompts = require('./prompts');
|
||||||
const balance = require('./balance');
|
const balance = require('./balance');
|
||||||
const plugins = require('./plugins');
|
const plugins = require('./plugins');
|
||||||
const bedrock = require('./bedrock');
|
const bedrock = require('./bedrock');
|
||||||
|
const actions = require('./actions');
|
||||||
const search = require('./search');
|
const search = require('./search');
|
||||||
const models = require('./models');
|
const models = require('./models');
|
||||||
const convos = require('./convos');
|
const convos = require('./convos');
|
||||||
|
|
@ -45,6 +46,7 @@ module.exports = {
|
||||||
config,
|
config,
|
||||||
models,
|
models,
|
||||||
plugins,
|
plugins,
|
||||||
|
actions,
|
||||||
presets,
|
presets,
|
||||||
balance,
|
balance,
|
||||||
messages,
|
messages,
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,28 @@
|
||||||
|
const jwt = require('jsonwebtoken');
|
||||||
|
const { nanoid } = require('nanoid');
|
||||||
|
const { tool } = require('@langchain/core/tools');
|
||||||
|
const { GraphEvents, sleep } = require('@librechat/agents');
|
||||||
const {
|
const {
|
||||||
|
Time,
|
||||||
CacheKeys,
|
CacheKeys,
|
||||||
|
StepTypes,
|
||||||
Constants,
|
Constants,
|
||||||
AuthTypeEnum,
|
AuthTypeEnum,
|
||||||
actionDelimiter,
|
actionDelimiter,
|
||||||
isImageVisionTool,
|
isImageVisionTool,
|
||||||
actionDomainSeparator,
|
actionDomainSeparator,
|
||||||
} = require('librechat-data-provider');
|
} = require('librechat-data-provider');
|
||||||
const { tool } = require('@langchain/core/tools');
|
const { refreshAccessToken } = require('~/server/services/TokenService');
|
||||||
const { isActionDomainAllowed } = require('~/server/services/domains');
|
const { isActionDomainAllowed } = require('~/server/services/domains');
|
||||||
|
const { logger, getFlowStateManager, sendEvent } = require('~/config');
|
||||||
const { encryptV2, decryptV2 } = require('~/server/utils/crypto');
|
const { encryptV2, decryptV2 } = require('~/server/utils/crypto');
|
||||||
const { getActions, deleteActions } = require('~/models/Action');
|
const { getActions, deleteActions } = require('~/models/Action');
|
||||||
const { deleteAssistant } = require('~/models/Assistant');
|
const { deleteAssistant } = require('~/models/Assistant');
|
||||||
|
const { findToken } = require('~/models/Token');
|
||||||
const { logAxiosError } = require('~/utils');
|
const { logAxiosError } = require('~/utils');
|
||||||
const { getLogStores } = require('~/cache');
|
const { getLogStores } = require('~/cache');
|
||||||
const { logger } = require('~/config');
|
|
||||||
|
|
||||||
|
const JWT_SECRET = process.env.JWT_SECRET;
|
||||||
const toolNameRegex = /^[a-zA-Z0-9_-]+$/;
|
const toolNameRegex = /^[a-zA-Z0-9_-]+$/;
|
||||||
const replaceSeparatorRegex = new RegExp(actionDomainSeparator, 'g');
|
const replaceSeparatorRegex = new RegExp(actionDomainSeparator, 'g');
|
||||||
|
|
||||||
|
|
@ -115,6 +123,8 @@ async function loadActionSets(searchParams) {
|
||||||
* Creates a general tool for an entire action set.
|
* Creates a general tool for an entire action set.
|
||||||
*
|
*
|
||||||
* @param {Object} params - The parameters for loading action sets.
|
* @param {Object} params - The parameters for loading action sets.
|
||||||
|
* @param {ServerRequest} params.req
|
||||||
|
* @param {ServerResponse} params.res
|
||||||
* @param {Action} params.action - The action set. Necessary for decrypting authentication values.
|
* @param {Action} params.action - The action set. Necessary for decrypting authentication values.
|
||||||
* @param {ActionRequest} params.requestBuilder - The ActionRequest builder class to execute the API call.
|
* @param {ActionRequest} params.requestBuilder - The ActionRequest builder class to execute the API call.
|
||||||
* @param {string | undefined} [params.name] - The name of the tool.
|
* @param {string | undefined} [params.name] - The name of the tool.
|
||||||
|
|
@ -122,33 +132,185 @@ async function loadActionSets(searchParams) {
|
||||||
* @param {import('zod').ZodTypeAny | undefined} [params.zodSchema] - The Zod schema for tool input validation/definition
|
* @param {import('zod').ZodTypeAny | undefined} [params.zodSchema] - The Zod schema for tool input validation/definition
|
||||||
* @returns { Promise<typeof tool | { _call: (toolInput: Object | string) => unknown}> } An object with `_call` method to execute the tool input.
|
* @returns { Promise<typeof tool | { _call: (toolInput: Object | string) => unknown}> } An object with `_call` method to execute the tool input.
|
||||||
*/
|
*/
|
||||||
async function createActionTool({ action, requestBuilder, zodSchema, name, description }) {
|
async function createActionTool({
|
||||||
action.metadata = await decryptMetadata(action.metadata);
|
req,
|
||||||
|
res,
|
||||||
|
action,
|
||||||
|
requestBuilder,
|
||||||
|
zodSchema,
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
}) {
|
||||||
const isDomainAllowed = await isActionDomainAllowed(action.metadata.domain);
|
const isDomainAllowed = await isActionDomainAllowed(action.metadata.domain);
|
||||||
if (!isDomainAllowed) {
|
if (!isDomainAllowed) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
/** @type {(toolInput: Object | string) => Promise<unknown>} */
|
const encrypted = {
|
||||||
const _call = async (toolInput) => {
|
oauth_client_id: action.metadata.oauth_client_id,
|
||||||
try {
|
oauth_client_secret: action.metadata.oauth_client_secret,
|
||||||
const executor = requestBuilder.createExecutor();
|
};
|
||||||
|
action.metadata = await decryptMetadata(action.metadata);
|
||||||
|
|
||||||
// Chain the operations
|
/** @type {(toolInput: Object | string, config: GraphRunnableConfig) => Promise<unknown>} */
|
||||||
|
const _call = async (toolInput, config) => {
|
||||||
|
try {
|
||||||
|
/** @type {import('librechat-data-provider').ActionMetadataRuntime} */
|
||||||
|
const metadata = action.metadata;
|
||||||
|
const executor = requestBuilder.createExecutor();
|
||||||
const preparedExecutor = executor.setParams(toolInput);
|
const preparedExecutor = executor.setParams(toolInput);
|
||||||
|
|
||||||
if (action.metadata.auth && action.metadata.auth.type !== AuthTypeEnum.None) {
|
if (metadata.auth && metadata.auth.type !== AuthTypeEnum.None) {
|
||||||
await preparedExecutor.setAuth(action.metadata);
|
try {
|
||||||
|
const action_id = action.action_id;
|
||||||
|
const identifier = `${req.user.id}:${action.action_id}`;
|
||||||
|
if (metadata.auth.type === AuthTypeEnum.OAuth && metadata.auth.authorization_url) {
|
||||||
|
const requestLogin = async () => {
|
||||||
|
const { args: _args, stepId, ...toolCall } = config.toolCall ?? {};
|
||||||
|
if (!stepId) {
|
||||||
|
throw new Error('Tool call is missing stepId');
|
||||||
|
}
|
||||||
|
const statePayload = {
|
||||||
|
nonce: nanoid(),
|
||||||
|
user: req.user.id,
|
||||||
|
action_id,
|
||||||
|
};
|
||||||
|
|
||||||
|
const stateToken = jwt.sign(statePayload, JWT_SECRET, { expiresIn: '10m' });
|
||||||
|
try {
|
||||||
|
const redirectUri = `${process.env.DOMAIN_CLIENT}/api/actions/${action_id}/oauth/callback`;
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
client_id: metadata.oauth_client_id,
|
||||||
|
scope: metadata.auth.scope,
|
||||||
|
redirect_uri: redirectUri,
|
||||||
|
access_type: 'offline',
|
||||||
|
response_type: 'code',
|
||||||
|
state: stateToken,
|
||||||
|
});
|
||||||
|
|
||||||
|
const authURL = `${metadata.auth.authorization_url}?${params.toString()}`;
|
||||||
|
/** @type {{ id: string; delta: AgentToolCallDelta }} */
|
||||||
|
const data = {
|
||||||
|
id: stepId,
|
||||||
|
delta: {
|
||||||
|
type: StepTypes.TOOL_CALLS,
|
||||||
|
tool_calls: [{ ...toolCall, args: '' }],
|
||||||
|
auth: authURL,
|
||||||
|
expires_at: Date.now() + Time.TWO_MINUTES,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const flowManager = await getFlowStateManager(getLogStores);
|
||||||
|
await flowManager.createFlowWithHandler(
|
||||||
|
`${identifier}:login`,
|
||||||
|
'oauth_login',
|
||||||
|
async () => {
|
||||||
|
sendEvent(res, { event: GraphEvents.ON_RUN_STEP_DELTA, data });
|
||||||
|
logger.debug('Sent OAuth login request to client', { action_id, identifier });
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
logger.debug('Waiting for OAuth Authorization response', { action_id, identifier });
|
||||||
|
const result = await flowManager.createFlow(identifier, 'oauth', {
|
||||||
|
state: stateToken,
|
||||||
|
userId: req.user.id,
|
||||||
|
client_url: metadata.auth.client_url,
|
||||||
|
redirect_uri: `${process.env.DOMAIN_CLIENT}/api/actions/${action_id}/oauth/callback`,
|
||||||
|
/** Encrypted values */
|
||||||
|
encrypted_oauth_client_id: encrypted.oauth_client_id,
|
||||||
|
encrypted_oauth_client_secret: encrypted.oauth_client_secret,
|
||||||
|
});
|
||||||
|
logger.debug('Received OAuth Authorization response', { action_id, identifier });
|
||||||
|
data.delta.auth = undefined;
|
||||||
|
data.delta.expires_at = undefined;
|
||||||
|
sendEvent(res, { event: GraphEvents.ON_RUN_STEP_DELTA, data });
|
||||||
|
await sleep(3000);
|
||||||
|
metadata.oauth_access_token = result.access_token;
|
||||||
|
metadata.oauth_refresh_token = result.refresh_token;
|
||||||
|
const expiresAt = new Date(Date.now() + result.expires_in * 1000);
|
||||||
|
metadata.oauth_token_expires_at = expiresAt.toISOString();
|
||||||
|
} catch (error) {
|
||||||
|
const errorMessage = 'Failed to authenticate OAuth tool';
|
||||||
|
logger.error(errorMessage, error);
|
||||||
|
throw new Error(errorMessage);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const tokenPromises = [];
|
||||||
|
tokenPromises.push(findToken({ userId: req.user.id, type: 'oauth', identifier }));
|
||||||
|
tokenPromises.push(
|
||||||
|
findToken({
|
||||||
|
userId: req.user.id,
|
||||||
|
type: 'oauth_refresh',
|
||||||
|
identifier: `${identifier}:refresh`,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
const [tokenData, refreshTokenData] = await Promise.all(tokenPromises);
|
||||||
|
|
||||||
|
if (tokenData) {
|
||||||
|
// Valid token exists, add it to metadata for setAuth
|
||||||
|
metadata.oauth_access_token = await decryptV2(tokenData.token);
|
||||||
|
if (refreshTokenData) {
|
||||||
|
metadata.oauth_refresh_token = await decryptV2(refreshTokenData.token);
|
||||||
|
}
|
||||||
|
metadata.oauth_token_expires_at = tokenData.expiresAt.toISOString();
|
||||||
|
} else if (!refreshTokenData) {
|
||||||
|
// No tokens exist, need to authenticate
|
||||||
|
await requestLogin();
|
||||||
|
} else if (refreshTokenData) {
|
||||||
|
// Refresh token is still valid, use it to get new access token
|
||||||
|
try {
|
||||||
|
const refresh_token = await decryptV2(refreshTokenData.token);
|
||||||
|
const refreshTokens = async () =>
|
||||||
|
await refreshAccessToken({
|
||||||
|
identifier,
|
||||||
|
refresh_token,
|
||||||
|
userId: req.user.id,
|
||||||
|
client_url: metadata.auth.client_url,
|
||||||
|
encrypted_oauth_client_id: encrypted.oauth_client_id,
|
||||||
|
encrypted_oauth_client_secret: encrypted.oauth_client_secret,
|
||||||
|
});
|
||||||
|
const flowManager = await getFlowStateManager(getLogStores);
|
||||||
|
const refreshData = await flowManager.createFlowWithHandler(
|
||||||
|
`${identifier}:refresh`,
|
||||||
|
'oauth_refresh',
|
||||||
|
refreshTokens,
|
||||||
|
);
|
||||||
|
metadata.oauth_access_token = refreshData.access_token;
|
||||||
|
if (refreshData.refresh_token) {
|
||||||
|
metadata.oauth_refresh_token = refreshData.refresh_token;
|
||||||
|
}
|
||||||
|
const expiresAt = new Date(Date.now() + refreshData.expires_in * 1000);
|
||||||
|
metadata.oauth_token_expires_at = expiresAt.toISOString();
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Failed to refresh token, requesting new login:', error);
|
||||||
|
await requestLogin();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
await requestLogin();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await preparedExecutor.setAuth(metadata);
|
||||||
|
} catch (error) {
|
||||||
|
if (
|
||||||
|
error.message.includes('No access token found') ||
|
||||||
|
error.message.includes('Access token is expired')
|
||||||
|
) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
throw new Error(`Authentication failed: ${error.message}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const res = await preparedExecutor.execute();
|
const response = await preparedExecutor.execute();
|
||||||
|
|
||||||
if (typeof res.data === 'object') {
|
if (typeof response.data === 'object') {
|
||||||
return JSON.stringify(res.data);
|
return JSON.stringify(response.data);
|
||||||
}
|
}
|
||||||
return res.data;
|
return response.data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const logMessage = `API call to ${action.metadata.domain} failed`;
|
const logMessage = `API call to ${action.metadata.domain} failed`;
|
||||||
logAxiosError({ message: logMessage, error });
|
logAxiosError({ message: logMessage, error });
|
||||||
|
throw error;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ const getBedrockOptions = require('~/server/services/Endpoints/bedrock/options')
|
||||||
const initOpenAI = require('~/server/services/Endpoints/openAI/initialize');
|
const initOpenAI = require('~/server/services/Endpoints/openAI/initialize');
|
||||||
const initCustom = require('~/server/services/Endpoints/custom/initialize');
|
const initCustom = require('~/server/services/Endpoints/custom/initialize');
|
||||||
const initGoogle = require('~/server/services/Endpoints/google/initialize');
|
const initGoogle = require('~/server/services/Endpoints/google/initialize');
|
||||||
|
const generateArtifactsPrompt = require('~/app/clients/prompts/artifacts');
|
||||||
const { getCustomEndpointConfig } = require('~/server/services/Config');
|
const { getCustomEndpointConfig } = require('~/server/services/Config');
|
||||||
const { loadAgentTools } = require('~/server/services/ToolService');
|
const { loadAgentTools } = require('~/server/services/ToolService');
|
||||||
const AgentClient = require('~/server/controllers/agents/client');
|
const AgentClient = require('~/server/controllers/agents/client');
|
||||||
|
|
@ -72,6 +73,16 @@ const primeResources = async (_attachments, _tool_resources) => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {object} params
|
||||||
|
* @param {ServerRequest} params.req
|
||||||
|
* @param {ServerResponse} params.res
|
||||||
|
* @param {Agent} params.agent
|
||||||
|
* @param {object} [params.endpointOption]
|
||||||
|
* @param {AgentToolResources} [params.tool_resources]
|
||||||
|
* @param {boolean} [params.isInitialAgent]
|
||||||
|
* @returns {Promise<Agent>}
|
||||||
|
*/
|
||||||
const initializeAgentOptions = async ({
|
const initializeAgentOptions = async ({
|
||||||
req,
|
req,
|
||||||
res,
|
res,
|
||||||
|
|
@ -82,6 +93,7 @@ const initializeAgentOptions = async ({
|
||||||
}) => {
|
}) => {
|
||||||
const { tools, toolContextMap } = await loadAgentTools({
|
const { tools, toolContextMap } = await loadAgentTools({
|
||||||
req,
|
req,
|
||||||
|
res,
|
||||||
agent,
|
agent,
|
||||||
tool_resources,
|
tool_resources,
|
||||||
});
|
});
|
||||||
|
|
@ -131,6 +143,13 @@ const initializeAgentOptions = async ({
|
||||||
agent.model_parameters.model = agent.model;
|
agent.model_parameters.model = agent.model;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (typeof agent.artifacts === 'string' && agent.artifacts !== '') {
|
||||||
|
agent.additional_instructions = generateArtifactsPrompt({
|
||||||
|
endpoint: agent.provider,
|
||||||
|
artifacts: agent.artifacts,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const tokensModel =
|
const tokensModel =
|
||||||
agent.provider === EModelEndpoint.azureOpenAI ? agent.model : agent.model_parameters.model;
|
agent.provider === EModelEndpoint.azureOpenAI ? agent.model : agent.model_parameters.model;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ const generateArtifactsPrompt = require('~/app/clients/prompts/artifacts');
|
||||||
const { getAssistant } = require('~/models/Assistant');
|
const { getAssistant } = require('~/models/Assistant');
|
||||||
|
|
||||||
const buildOptions = async (endpoint, parsedBody) => {
|
const buildOptions = async (endpoint, parsedBody) => {
|
||||||
// eslint-disable-next-line no-unused-vars
|
|
||||||
const { promptPrefix, assistant_id, iconURL, greeting, spec, artifacts, ...modelOptions } =
|
const { promptPrefix, assistant_id, iconURL, greeting, spec, artifacts, ...modelOptions } =
|
||||||
parsedBody;
|
parsedBody;
|
||||||
const endpointOption = removeNullishValues({
|
const endpointOption = removeNullishValues({
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ const generateArtifactsPrompt = require('~/app/clients/prompts/artifacts');
|
||||||
const { getAssistant } = require('~/models/Assistant');
|
const { getAssistant } = require('~/models/Assistant');
|
||||||
|
|
||||||
const buildOptions = async (endpoint, parsedBody) => {
|
const buildOptions = async (endpoint, parsedBody) => {
|
||||||
// eslint-disable-next-line no-unused-vars
|
|
||||||
const { promptPrefix, assistant_id, iconURL, greeting, spec, artifacts, ...modelOptions } =
|
const { promptPrefix, assistant_id, iconURL, greeting, spec, artifacts, ...modelOptions } =
|
||||||
parsedBody;
|
parsedBody;
|
||||||
const endpointOption = removeNullishValues({
|
const endpointOption = removeNullishValues({
|
||||||
|
|
|
||||||
170
api/server/services/TokenService.js
Normal file
170
api/server/services/TokenService.js
Normal file
|
|
@ -0,0 +1,170 @@
|
||||||
|
const axios = require('axios');
|
||||||
|
const { handleOAuthToken } = require('~/models/Token');
|
||||||
|
const { decryptV2 } = require('~/server/utils/crypto');
|
||||||
|
const { logAxiosError } = require('~/utils');
|
||||||
|
const { logger } = require('~/config');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Processes the access tokens and stores them in the database.
|
||||||
|
* @param {object} tokenData
|
||||||
|
* @param {string} tokenData.access_token
|
||||||
|
* @param {number} tokenData.expires_in
|
||||||
|
* @param {string} [tokenData.refresh_token]
|
||||||
|
* @param {number} [tokenData.refresh_token_expires_in]
|
||||||
|
* @param {object} metadata
|
||||||
|
* @param {string} metadata.userId
|
||||||
|
* @param {string} metadata.identifier
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
async function processAccessTokens(tokenData, { userId, identifier }) {
|
||||||
|
const { access_token, expires_in = 3600, refresh_token, refresh_token_expires_in } = tokenData;
|
||||||
|
if (!access_token) {
|
||||||
|
logger.error('Access token not found: ', tokenData);
|
||||||
|
throw new Error('Access token not found');
|
||||||
|
}
|
||||||
|
await handleOAuthToken({
|
||||||
|
identifier,
|
||||||
|
token: access_token,
|
||||||
|
expiresIn: expires_in,
|
||||||
|
userId,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (refresh_token != null) {
|
||||||
|
logger.debug('Processing refresh token');
|
||||||
|
await handleOAuthToken({
|
||||||
|
token: refresh_token,
|
||||||
|
type: 'oauth_refresh',
|
||||||
|
userId,
|
||||||
|
identifier: `${identifier}:refresh`,
|
||||||
|
expiresIn: refresh_token_expires_in ?? null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
logger.debug('Access tokens processed');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Refreshes the access token using the refresh token.
|
||||||
|
* @param {object} fields
|
||||||
|
* @param {string} fields.userId - The ID of the user.
|
||||||
|
* @param {string} fields.client_url - The URL of the OAuth provider.
|
||||||
|
* @param {string} fields.identifier - The identifier for the token.
|
||||||
|
* @param {string} fields.refresh_token - The refresh token to use.
|
||||||
|
* @param {string} fields.encrypted_oauth_client_id - The client ID for the OAuth provider.
|
||||||
|
* @param {string} fields.encrypted_oauth_client_secret - The client secret for the OAuth provider.
|
||||||
|
* @returns {Promise<{
|
||||||
|
* access_token: string,
|
||||||
|
* expires_in: number,
|
||||||
|
* refresh_token?: string,
|
||||||
|
* refresh_token_expires_in?: number,
|
||||||
|
* }>}
|
||||||
|
*/
|
||||||
|
const refreshAccessToken = async ({
|
||||||
|
userId,
|
||||||
|
client_url,
|
||||||
|
identifier,
|
||||||
|
refresh_token,
|
||||||
|
encrypted_oauth_client_id,
|
||||||
|
encrypted_oauth_client_secret,
|
||||||
|
}) => {
|
||||||
|
try {
|
||||||
|
const oauth_client_id = await decryptV2(encrypted_oauth_client_id);
|
||||||
|
const oauth_client_secret = await decryptV2(encrypted_oauth_client_secret);
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
client_id: oauth_client_id,
|
||||||
|
client_secret: oauth_client_secret,
|
||||||
|
grant_type: 'refresh_token',
|
||||||
|
refresh_token,
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await axios({
|
||||||
|
method: 'POST',
|
||||||
|
url: client_url,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
|
Accept: 'application/json',
|
||||||
|
},
|
||||||
|
data: params.toString(),
|
||||||
|
});
|
||||||
|
await processAccessTokens(response.data, {
|
||||||
|
userId,
|
||||||
|
identifier,
|
||||||
|
});
|
||||||
|
logger.debug(`Access token refreshed successfully for ${identifier}`);
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
const message = 'Error refreshing OAuth tokens';
|
||||||
|
logAxiosError({
|
||||||
|
message,
|
||||||
|
error,
|
||||||
|
});
|
||||||
|
throw new Error(message);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles the OAuth callback and exchanges the authorization code for tokens.
|
||||||
|
* @param {object} fields
|
||||||
|
* @param {string} fields.code - The authorization code returned by the provider.
|
||||||
|
* @param {string} fields.userId - The ID of the user.
|
||||||
|
* @param {string} fields.identifier - The identifier for the token.
|
||||||
|
* @param {string} fields.client_url - The URL of the OAuth provider.
|
||||||
|
* @param {string} fields.redirect_uri - The redirect URI for the OAuth provider.
|
||||||
|
* @param {string} fields.encrypted_oauth_client_id - The client ID for the OAuth provider.
|
||||||
|
* @param {string} fields.encrypted_oauth_client_secret - The client secret for the OAuth provider.
|
||||||
|
* @returns {Promise<{
|
||||||
|
* access_token: string,
|
||||||
|
* expires_in: number,
|
||||||
|
* refresh_token?: string,
|
||||||
|
* refresh_token_expires_in?: number,
|
||||||
|
* }>}
|
||||||
|
*/
|
||||||
|
const getAccessToken = async ({
|
||||||
|
code,
|
||||||
|
userId,
|
||||||
|
identifier,
|
||||||
|
client_url,
|
||||||
|
redirect_uri,
|
||||||
|
encrypted_oauth_client_id,
|
||||||
|
encrypted_oauth_client_secret,
|
||||||
|
}) => {
|
||||||
|
const oauth_client_id = await decryptV2(encrypted_oauth_client_id);
|
||||||
|
const oauth_client_secret = await decryptV2(encrypted_oauth_client_secret);
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
code,
|
||||||
|
client_id: oauth_client_id,
|
||||||
|
client_secret: oauth_client_secret,
|
||||||
|
grant_type: 'authorization_code',
|
||||||
|
redirect_uri,
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await axios({
|
||||||
|
method: 'POST',
|
||||||
|
url: client_url,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
|
Accept: 'application/json',
|
||||||
|
},
|
||||||
|
data: params.toString(),
|
||||||
|
});
|
||||||
|
|
||||||
|
await processAccessTokens(response.data, {
|
||||||
|
userId,
|
||||||
|
identifier,
|
||||||
|
});
|
||||||
|
logger.debug(`Access tokens successfully created for ${identifier}`);
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
const message = 'Error exchanging OAuth code';
|
||||||
|
logAxiosError({
|
||||||
|
message,
|
||||||
|
error,
|
||||||
|
});
|
||||||
|
throw new Error(message);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
getAccessToken,
|
||||||
|
refreshAccessToken,
|
||||||
|
};
|
||||||
|
|
@ -409,11 +409,12 @@ async function processRequiredActions(client, requiredActions) {
|
||||||
* Processes the runtime tool calls and returns the tool classes.
|
* Processes the runtime tool calls and returns the tool classes.
|
||||||
* @param {Object} params - Run params containing user and request information.
|
* @param {Object} params - Run params containing user and request information.
|
||||||
* @param {ServerRequest} params.req - The request object.
|
* @param {ServerRequest} params.req - The request object.
|
||||||
|
* @param {ServerResponse} params.res - The request object.
|
||||||
* @param {Agent} params.agent - The agent to load tools for.
|
* @param {Agent} params.agent - The agent to load tools for.
|
||||||
* @param {string | undefined} [params.openAIApiKey] - The OpenAI API key.
|
* @param {string | undefined} [params.openAIApiKey] - The OpenAI API key.
|
||||||
* @returns {Promise<{ tools?: StructuredTool[] }>} The agent tools.
|
* @returns {Promise<{ tools?: StructuredTool[] }>} The agent tools.
|
||||||
*/
|
*/
|
||||||
async function loadAgentTools({ req, agent, tool_resources, openAIApiKey }) {
|
async function loadAgentTools({ req, res, agent, tool_resources, openAIApiKey }) {
|
||||||
if (!agent.tools || agent.tools.length === 0) {
|
if (!agent.tools || agent.tools.length === 0) {
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
@ -546,6 +547,8 @@ async function loadAgentTools({ req, agent, tool_resources, openAIApiKey }) {
|
||||||
|
|
||||||
if (requestBuilder) {
|
if (requestBuilder) {
|
||||||
const tool = await createActionTool({
|
const tool = await createActionTool({
|
||||||
|
req,
|
||||||
|
res,
|
||||||
action: actionSet,
|
action: actionSet,
|
||||||
requestBuilder,
|
requestBuilder,
|
||||||
zodSchema,
|
zodSchema,
|
||||||
|
|
|
||||||
|
|
@ -200,6 +200,7 @@ function generateConfig(key, baseURL, endpoint) {
|
||||||
config.capabilities = [
|
config.capabilities = [
|
||||||
AgentCapabilities.execute_code,
|
AgentCapabilities.execute_code,
|
||||||
AgentCapabilities.file_search,
|
AgentCapabilities.file_search,
|
||||||
|
AgentCapabilities.artifacts,
|
||||||
AgentCapabilities.actions,
|
AgentCapabilities.actions,
|
||||||
AgentCapabilities.tools,
|
AgentCapabilities.tools,
|
||||||
];
|
];
|
||||||
|
|
|
||||||
|
|
@ -98,6 +98,12 @@
|
||||||
* @memberof typedefs
|
* @memberof typedefs
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @exports LangChainToolCall
|
||||||
|
* @typedef {import('@langchain/core/messages/tool').ToolCall} LangChainToolCall
|
||||||
|
* @memberof typedefs
|
||||||
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @exports GraphRunnableConfig
|
* @exports GraphRunnableConfig
|
||||||
* @typedef {import('@langchain/core/runnables').RunnableConfig<{
|
* @typedef {import('@langchain/core/runnables').RunnableConfig<{
|
||||||
|
|
@ -109,7 +115,9 @@
|
||||||
* agent_index: number;
|
* agent_index: number;
|
||||||
* last_agent_index: number;
|
* last_agent_index: number;
|
||||||
* hide_sequential_outputs: boolean;
|
* hide_sequential_outputs: boolean;
|
||||||
* }>} GraphRunnableConfig
|
* }> & {
|
||||||
|
* toolCall?: LangChainToolCall & { stepId?: string };
|
||||||
|
* }} GraphRunnableConfig
|
||||||
* @memberof typedefs
|
* @memberof typedefs
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
@ -383,6 +391,12 @@
|
||||||
* @memberof typedefs
|
* @memberof typedefs
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @exports AgentToolCallDelta
|
||||||
|
* @typedef {import('librechat-data-provider').Agents.ToolCallDelta} AgentToolCallDelta
|
||||||
|
* @memberof typedefs
|
||||||
|
*/
|
||||||
|
|
||||||
/** Prompts */
|
/** Prompts */
|
||||||
/**
|
/**
|
||||||
* @exports TPrompt
|
* @exports TPrompt
|
||||||
|
|
@ -947,12 +961,24 @@
|
||||||
* @memberof typedefs
|
* @memberof typedefs
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @exports Keyv
|
||||||
|
* @typedef {import('keyv')} Keyv
|
||||||
|
* @memberof typedefs
|
||||||
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @exports MCPManager
|
* @exports MCPManager
|
||||||
* @typedef {import('librechat-mcp').MCPManager} MCPManager
|
* @typedef {import('librechat-mcp').MCPManager} MCPManager
|
||||||
* @memberof typedefs
|
* @memberof typedefs
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @exports FlowStateManager
|
||||||
|
* @typedef {import('librechat-mcp').FlowStateManager} FlowStateManager
|
||||||
|
* @memberof typedefs
|
||||||
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @exports LCAvailableTools
|
* @exports LCAvailableTools
|
||||||
* @typedef {import('librechat-mcp').LCAvailableTools} LCAvailableTools
|
* @typedef {import('librechat-mcp').LCAvailableTools} LCAvailableTools
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@librechat/frontend",
|
"name": "@librechat/frontend",
|
||||||
"version": "v0.7.6",
|
"version": "v0.7.7-rc1",
|
||||||
"description": "",
|
"description": "",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|
@ -50,11 +50,8 @@
|
||||||
"@radix-ui/react-switch": "^1.0.3",
|
"@radix-ui/react-switch": "^1.0.3",
|
||||||
"@radix-ui/react-tabs": "^1.0.3",
|
"@radix-ui/react-tabs": "^1.0.3",
|
||||||
"@radix-ui/react-toast": "^1.1.5",
|
"@radix-ui/react-toast": "^1.1.5",
|
||||||
"@radix-ui/react-tooltip": "^1.0.6",
|
|
||||||
"@tanstack/react-query": "^4.28.0",
|
"@tanstack/react-query": "^4.28.0",
|
||||||
"@tanstack/react-table": "^8.11.7",
|
"@tanstack/react-table": "^8.11.7",
|
||||||
"@zattoo/use-double-click": "1.2.0",
|
|
||||||
"axios": "^1.7.7",
|
|
||||||
"class-variance-authority": "^0.6.0",
|
"class-variance-authority": "^0.6.0",
|
||||||
"clsx": "^1.2.1",
|
"clsx": "^1.2.1",
|
||||||
"copy-to-clipboard": "^3.3.3",
|
"copy-to-clipboard": "^3.3.3",
|
||||||
|
|
@ -65,7 +62,7 @@
|
||||||
"filenamify": "^6.0.0",
|
"filenamify": "^6.0.0",
|
||||||
"framer-motion": "^11.5.4",
|
"framer-motion": "^11.5.4",
|
||||||
"html-to-image": "^1.11.11",
|
"html-to-image": "^1.11.11",
|
||||||
"image-blob-reduce": "^4.1.0",
|
"i18next": "^24.2.2",
|
||||||
"js-cookie": "^3.0.5",
|
"js-cookie": "^3.0.5",
|
||||||
"librechat-data-provider": "*",
|
"librechat-data-provider": "*",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
|
|
@ -79,10 +76,10 @@
|
||||||
"react-dnd": "^16.0.1",
|
"react-dnd": "^16.0.1",
|
||||||
"react-dnd-html5-backend": "^16.0.1",
|
"react-dnd-html5-backend": "^16.0.1",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-error-boundary": "^5.0.0",
|
|
||||||
"react-flip-toolkit": "^7.1.0",
|
"react-flip-toolkit": "^7.1.0",
|
||||||
"react-gtm-module": "^2.0.11",
|
"react-gtm-module": "^2.0.11",
|
||||||
"react-hook-form": "^7.43.9",
|
"react-hook-form": "^7.43.9",
|
||||||
|
"react-i18next": "^15.4.0",
|
||||||
"react-lazy-load-image-component": "^1.6.0",
|
"react-lazy-load-image-component": "^1.6.0",
|
||||||
"react-markdown": "^9.0.1",
|
"react-markdown": "^9.0.1",
|
||||||
"react-resizable-panels": "^2.1.1",
|
"react-resizable-panels": "^2.1.1",
|
||||||
|
|
@ -103,7 +100,6 @@
|
||||||
"tailwind-merge": "^1.9.1",
|
"tailwind-merge": "^1.9.1",
|
||||||
"tailwindcss-animate": "^1.0.5",
|
"tailwindcss-animate": "^1.0.5",
|
||||||
"tailwindcss-radix": "^2.8.0",
|
"tailwindcss-radix": "^2.8.0",
|
||||||
"url": "^0.11.0",
|
|
||||||
"zod": "^3.22.4"
|
"zod": "^3.22.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|
@ -116,31 +112,31 @@
|
||||||
"@testing-library/jest-dom": "^5.16.5",
|
"@testing-library/jest-dom": "^5.16.5",
|
||||||
"@testing-library/react": "^14.0.0",
|
"@testing-library/react": "^14.0.0",
|
||||||
"@testing-library/user-event": "^14.4.3",
|
"@testing-library/user-event": "^14.4.3",
|
||||||
"@types/jest": "^29.5.2",
|
"@types/jest": "^29.5.14",
|
||||||
"@types/js-cookie": "^3.0.6",
|
"@types/js-cookie": "^3.0.6",
|
||||||
"@types/node": "^20.3.0",
|
"@types/node": "^20.3.0",
|
||||||
"@types/react": "^18.2.11",
|
"@types/react": "^18.2.11",
|
||||||
"@types/react-dom": "^18.2.4",
|
"@types/react-dom": "^18.2.4",
|
||||||
"@vitejs/plugin-react": "^4.2.1",
|
"@vitejs/plugin-react": "^4.2.1",
|
||||||
"autoprefixer": "^10.4.13",
|
"autoprefixer": "^10.4.20",
|
||||||
"babel-plugin-replace-ts-export-assignment": "^0.0.2",
|
"babel-plugin-replace-ts-export-assignment": "^0.0.2",
|
||||||
"babel-plugin-root-import": "^6.6.0",
|
"babel-plugin-root-import": "^6.6.0",
|
||||||
"babel-plugin-transform-import-meta": "^2.2.1",
|
"babel-plugin-transform-import-meta": "^2.3.2",
|
||||||
"babel-plugin-transform-vite-meta-env": "^1.0.3",
|
"babel-plugin-transform-vite-meta-env": "^1.0.3",
|
||||||
"eslint-plugin-jest": "^27.2.1",
|
"eslint-plugin-jest": "^28.11.0",
|
||||||
"identity-obj-proxy": "^3.0.0",
|
"identity-obj-proxy": "^3.0.0",
|
||||||
"jest": "^29.5.0",
|
"jest": "^29.7.0",
|
||||||
"jest-canvas-mock": "^2.5.1",
|
"jest-canvas-mock": "^2.5.2",
|
||||||
"jest-environment-jsdom": "^29.5.0",
|
"jest-environment-jsdom": "^29.7.0",
|
||||||
"jest-file-loader": "^1.0.3",
|
"jest-file-loader": "^1.0.3",
|
||||||
"jest-junit": "^16.0.0",
|
"jest-junit": "^16.0.0",
|
||||||
"postcss": "^8.4.31",
|
"postcss": "^8.4.31",
|
||||||
"postcss-loader": "^7.1.0",
|
"postcss-loader": "^7.1.0",
|
||||||
"postcss-preset-env": "^8.2.0",
|
"postcss-preset-env": "^8.2.0",
|
||||||
"tailwindcss": "^3.4.1",
|
"tailwindcss": "^3.4.1",
|
||||||
"ts-jest": "^29.1.0",
|
"ts-jest": "^29.2.5",
|
||||||
"typescript": "^5.0.4",
|
"typescript": "^5.3.3",
|
||||||
"vite": "^5.4.14",
|
"vite": "^6.1.0",
|
||||||
"vite-plugin-node-polyfills": "^0.17.0",
|
"vite-plugin-node-polyfills": "^0.17.0",
|
||||||
"vite-plugin-pwa": "^0.21.1"
|
"vite-plugin-pwa": "^0.21.1"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { AgentCapabilities } from 'librechat-data-provider';
|
import { AgentCapabilities, ArtifactModes } from 'librechat-data-provider';
|
||||||
import type { Agent, AgentProvider, AgentModelParameters } from 'librechat-data-provider';
|
import type { Agent, AgentProvider, AgentModelParameters } from 'librechat-data-provider';
|
||||||
import type { OptionWithIcon, ExtendedFile } from './types';
|
import type { OptionWithIcon, ExtendedFile } from './types';
|
||||||
|
|
||||||
|
|
@ -9,8 +9,8 @@ export type TAgentOption = OptionWithIcon &
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TAgentCapabilities = {
|
export type TAgentCapabilities = {
|
||||||
[AgentCapabilities.execute_code]: boolean;
|
|
||||||
[AgentCapabilities.file_search]: boolean;
|
[AgentCapabilities.file_search]: boolean;
|
||||||
|
[AgentCapabilities.execute_code]: boolean;
|
||||||
[AgentCapabilities.end_after_tools]?: boolean;
|
[AgentCapabilities.end_after_tools]?: boolean;
|
||||||
[AgentCapabilities.hide_sequential_outputs]?: boolean;
|
[AgentCapabilities.hide_sequential_outputs]?: boolean;
|
||||||
};
|
};
|
||||||
|
|
@ -26,4 +26,5 @@ export type AgentForm = {
|
||||||
tools?: string[];
|
tools?: string[];
|
||||||
provider?: AgentProvider | OptionWithIcon;
|
provider?: AgentProvider | OptionWithIcon;
|
||||||
agent_ids?: string[];
|
agent_ids?: string[];
|
||||||
|
[AgentCapabilities.artifacts]?: ArtifactModes | string;
|
||||||
} & TAgentCapabilities;
|
} & TAgentCapabilities;
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import type { SetterOrUpdater } from 'recoil';
|
||||||
import type * as t from 'librechat-data-provider';
|
import type * as t from 'librechat-data-provider';
|
||||||
import type { UseMutationResult } from '@tanstack/react-query';
|
import type { UseMutationResult } from '@tanstack/react-query';
|
||||||
import type { LucideIcon } from 'lucide-react';
|
import type { LucideIcon } from 'lucide-react';
|
||||||
|
import type { TranslationKeys } from '~/hooks';
|
||||||
|
|
||||||
export type CodeBarProps = {
|
export type CodeBarProps = {
|
||||||
lang: string;
|
lang: string;
|
||||||
|
|
@ -66,7 +67,10 @@ export type GenericSetter<T> = (value: T | ((currentValue: T) => T)) => void;
|
||||||
|
|
||||||
export type LastSelectedModels = Record<t.EModelEndpoint, string>;
|
export type LastSelectedModels = Record<t.EModelEndpoint, string>;
|
||||||
|
|
||||||
export type LocalizeFunction = (phraseKey: string, ...values: string[]) => string;
|
export type LocalizeFunction = (
|
||||||
|
phraseKey: TranslationKeys,
|
||||||
|
options?: Record<string, string | number>,
|
||||||
|
) => string;
|
||||||
|
|
||||||
export type ChatFormValues = { text: string };
|
export type ChatFormValues = { text: string };
|
||||||
|
|
||||||
|
|
@ -85,6 +89,7 @@ export type IconMapProps = {
|
||||||
iconURL?: string;
|
iconURL?: string;
|
||||||
context?: 'landing' | 'menu-item' | 'nav' | 'message';
|
context?: 'landing' | 'menu-item' | 'nav' | 'message';
|
||||||
endpoint?: string | null;
|
endpoint?: string | null;
|
||||||
|
endpointType?: string;
|
||||||
assistantName?: string;
|
assistantName?: string;
|
||||||
agentName?: string;
|
agentName?: string;
|
||||||
avatar?: string;
|
avatar?: string;
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { useLocalize } from '~/hooks';
|
import { TranslationKeys, useLocalize } from '~/hooks';
|
||||||
import { BlinkAnimation } from './BlinkAnimation';
|
import { BlinkAnimation } from './BlinkAnimation';
|
||||||
import { TStartupConfig } from 'librechat-data-provider';
|
import { TStartupConfig } from 'librechat-data-provider';
|
||||||
import SocialLoginRender from './SocialLoginRender';
|
import SocialLoginRender from './SocialLoginRender';
|
||||||
|
|
@ -33,7 +33,7 @@ function AuthLayout({
|
||||||
startupConfig: TStartupConfig | null | undefined;
|
startupConfig: TStartupConfig | null | undefined;
|
||||||
startupConfigError: unknown | null | undefined;
|
startupConfigError: unknown | null | undefined;
|
||||||
pathname: string;
|
pathname: string;
|
||||||
error: string | null;
|
error: TranslationKeys | null;
|
||||||
}) {
|
}) {
|
||||||
const localize = useLocalize();
|
const localize = useLocalize();
|
||||||
|
|
||||||
|
|
@ -65,7 +65,7 @@ function AuthLayout({
|
||||||
<img
|
<img
|
||||||
src="/assets/logo.svg"
|
src="/assets/logo.svg"
|
||||||
className="h-full w-full object-contain"
|
className="h-full w-full object-contain"
|
||||||
alt={localize('com_ui_logo', startupConfig?.appTitle ?? 'LibreChat')}
|
alt={localize('com_ui_logo', { 0: startupConfig?.appTitle ?? 'LibreChat' })}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</BlinkAnimation>
|
</BlinkAnimation>
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ import type { TRegisterUser, TError } from 'librechat-data-provider';
|
||||||
import type { TLoginLayoutContext } from '~/common';
|
import type { TLoginLayoutContext } from '~/common';
|
||||||
import { ErrorMessage } from './ErrorMessage';
|
import { ErrorMessage } from './ErrorMessage';
|
||||||
import { Spinner } from '~/components/svg';
|
import { Spinner } from '~/components/svg';
|
||||||
import { useLocalize } from '~/hooks';
|
import { useLocalize, TranslationKeys } from '~/hooks';
|
||||||
|
|
||||||
const Registration: React.FC = () => {
|
const Registration: React.FC = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
@ -56,7 +56,7 @@ const Registration: React.FC = () => {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const renderInput = (id: string, label: string, type: string, validation: object) => (
|
const renderInput = (id: string, label: TranslationKeys, type: string, validation: object) => (
|
||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<input
|
<input
|
||||||
|
|
@ -114,7 +114,7 @@ const Registration: React.FC = () => {
|
||||||
: 'com_auth_registration_success_insecure',
|
: 'com_auth_registration_success_insecure',
|
||||||
) +
|
) +
|
||||||
' ' +
|
' ' +
|
||||||
localize('com_auth_email_verification_redirecting', countdown.toString())}
|
localize('com_auth_email_verification_redirecting', { 0: countdown.toString() })}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{!startupConfigError && !isFetching && (
|
{!startupConfigError && !isFetching && (
|
||||||
|
|
|
||||||
|
|
@ -84,7 +84,7 @@ function RequestPasswordReset() {
|
||||||
</h1>
|
</h1>
|
||||||
{countdown > 0 && (
|
{countdown > 0 && (
|
||||||
<p className="text-center text-lg text-gray-600 dark:text-gray-400">
|
<p className="text-center text-lg text-gray-600 dark:text-gray-400">
|
||||||
{localize('com_auth_email_verification_redirecting', countdown.toString())}
|
{localize('com_auth_email_verification_redirecting', { 0: countdown.toString() })}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
{showResendLink && countdown === 0 && (
|
{showResendLink && countdown === 0 && (
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,7 @@ function ChatView({ index = 0 }: { index?: number }) {
|
||||||
select: useCallback(
|
select: useCallback(
|
||||||
(data: TMessage[]) => {
|
(data: TMessage[]) => {
|
||||||
const dataTree = buildTree({ messages: data, fileMap });
|
const dataTree = buildTree({ messages: data, fileMap });
|
||||||
return dataTree?.length === 0 ? null : dataTree ?? null;
|
return dataTree?.length === 0 ? null : (dataTree ?? null);
|
||||||
},
|
},
|
||||||
[fileMap],
|
[fileMap],
|
||||||
),
|
),
|
||||||
|
|
@ -62,7 +62,7 @@ function ChatView({ index = 0 }: { index?: number }) {
|
||||||
<ChatFormProvider {...methods}>
|
<ChatFormProvider {...methods}>
|
||||||
<ChatContext.Provider value={chatHelpers}>
|
<ChatContext.Provider value={chatHelpers}>
|
||||||
<AddedChatContext.Provider value={addedChatHelpers}>
|
<AddedChatContext.Provider value={addedChatHelpers}>
|
||||||
<Presentation useSidePanel={true}>
|
<Presentation>
|
||||||
{content}
|
{content}
|
||||||
<div className="w-full border-t-0 pl-0 pt-2 dark:border-white/20 md:w-[calc(100%-.5rem)] md:border-t-0 md:border-transparent md:pl-0 md:pt-0 md:dark:border-transparent">
|
<div className="w-full border-t-0 pl-0 pt-2 dark:border-white/20 md:w-[calc(100%-.5rem)] md:border-t-0 md:border-transparent md:pl-0 md:pt-0 md:dark:border-transparent">
|
||||||
<ChatForm index={index} />
|
<ChatForm index={index} />
|
||||||
|
|
|
||||||
|
|
@ -220,8 +220,10 @@ export default function DataTable<TData, TValue>({ columns, data }: DataTablePro
|
||||||
<span className="hidden sm:inline">
|
<span className="hidden sm:inline">
|
||||||
{localize(
|
{localize(
|
||||||
'com_files_number_selected',
|
'com_files_number_selected',
|
||||||
`${table.getFilteredSelectedRowModel().rows.length}`,
|
{
|
||||||
`${table.getFilteredRowModel().rows.length}`,
|
0: `${table.getFilteredSelectedRowModel().rows.length}`,
|
||||||
|
1: `${table.getFilteredRowModel().rows.length}`,
|
||||||
|
},
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
<span className="sm:hidden">
|
<span className="sm:hidden">
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ import {
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from '~/components/ui/DropdownMenu';
|
} from '~/components/ui/DropdownMenu';
|
||||||
import { Button } from '~/components/ui/Button';
|
import { Button } from '~/components/ui/Button';
|
||||||
import useLocalize from '~/hooks/useLocalize';
|
import { useLocalize, TranslationKeys } from '~/hooks';
|
||||||
import { cn } from '~/utils';
|
import { cn } from '~/utils';
|
||||||
|
|
||||||
interface SortFilterHeaderProps<TData, TValue> extends React.HTMLAttributes<HTMLDivElement> {
|
interface SortFilterHeaderProps<TData, TValue> extends React.HTMLAttributes<HTMLDivElement> {
|
||||||
|
|
@ -78,9 +78,12 @@ export function SortFilterHeader<TData, TValue>({
|
||||||
<DropdownMenuSeparator className="dark:bg-gray-500" />
|
<DropdownMenuSeparator className="dark:bg-gray-500" />
|
||||||
{filters &&
|
{filters &&
|
||||||
Object.entries(filters).map(([key, values]) =>
|
Object.entries(filters).map(([key, values]) =>
|
||||||
values.map((value: string | number) => {
|
values.map((value?: string | number) => {
|
||||||
const localizedValue = localize(valueMap?.[value] ?? '');
|
const translationKey = valueMap?.[value ?? ''];
|
||||||
const filterValue = localizedValue.length ? localizedValue : valueMap?.[value];
|
const filterValue =
|
||||||
|
translationKey != null && translationKey.length
|
||||||
|
? localize(translationKey as TranslationKeys)
|
||||||
|
: String(value);
|
||||||
if (!filterValue) {
|
if (!filterValue) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -44,8 +44,8 @@ export default function PopoverButtons({
|
||||||
const endpoint = overrideEndpoint ?? endpointType ?? _endpoint ?? '';
|
const endpoint = overrideEndpoint ?? endpointType ?? _endpoint ?? '';
|
||||||
const model = overrideModel ?? _model;
|
const model = overrideModel ?? _model;
|
||||||
|
|
||||||
const isGenerativeModel = model?.toLowerCase()?.includes('gemini') ?? false;
|
const isGenerativeModel = model?.toLowerCase().includes('gemini') ?? false;
|
||||||
const isChatModel = (!isGenerativeModel && model?.toLowerCase()?.includes('chat')) ?? false;
|
const isChatModel = (!isGenerativeModel && model?.toLowerCase().includes('chat')) ?? false;
|
||||||
const isTextModel = !isGenerativeModel && !isChatModel && /code|text/.test(model ?? '');
|
const isTextModel = !isGenerativeModel && !isChatModel && /code|text/.test(model ?? '');
|
||||||
|
|
||||||
const { showExamples } = optionSettings;
|
const { showExamples } = optionSettings;
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ export const TemporaryChat = ({ isTemporaryChat, setIsTemporaryChat }: Temporary
|
||||||
<div className="flex items-start gap-4 py-2.5 pl-3 pr-1.5 text-sm">
|
<div className="flex items-start gap-4 py-2.5 pl-3 pr-1.5 text-sm">
|
||||||
<span className="mt-0 flex h-6 w-6 flex-shrink-0 items-center justify-center">
|
<span className="mt-0 flex h-6 w-6 flex-shrink-0 items-center justify-center">
|
||||||
<div className="icon-md">
|
<div className="icon-md">
|
||||||
<MessageCircleDashed className="icon-md" />
|
<MessageCircleDashed className="icon-md" aria-hidden="true" />
|
||||||
</div>
|
</div>
|
||||||
</span>
|
</span>
|
||||||
<span className="text-token-text-secondary line-clamp-3 flex-1 py-0.5 font-semibold">
|
<span className="text-token-text-secondary line-clamp-3 flex-1 py-0.5 font-semibold">
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,9 @@ import FinishedIcon from './FinishedIcon';
|
||||||
import MarkdownLite from './MarkdownLite';
|
import MarkdownLite from './MarkdownLite';
|
||||||
import store from '~/store';
|
import store from '~/store';
|
||||||
|
|
||||||
|
const radius = 56.08695652173913;
|
||||||
|
const circumference = 2 * Math.PI * radius;
|
||||||
|
|
||||||
export default function CodeAnalyze({
|
export default function CodeAnalyze({
|
||||||
initialProgress = 0.1,
|
initialProgress = 0.1,
|
||||||
code,
|
code,
|
||||||
|
|
@ -22,9 +25,6 @@ export default function CodeAnalyze({
|
||||||
const progress = useProgress(initialProgress);
|
const progress = useProgress(initialProgress);
|
||||||
const showAnalysisCode = useRecoilValue(store.showCode);
|
const showAnalysisCode = useRecoilValue(store.showCode);
|
||||||
const [showCode, setShowCode] = useState(showAnalysisCode);
|
const [showCode, setShowCode] = useState(showAnalysisCode);
|
||||||
|
|
||||||
const radius = 56.08695652173913;
|
|
||||||
const circumference = 2 * Math.PI * radius;
|
|
||||||
const offset = circumference - progress * circumference;
|
const offset = circumference - progress * circumference;
|
||||||
|
|
||||||
const logs = outputs.reduce((acc, output) => {
|
const logs = outputs.reduce((acc, output) => {
|
||||||
|
|
@ -53,9 +53,10 @@ export default function CodeAnalyze({
|
||||||
<ProgressText
|
<ProgressText
|
||||||
progress={progress}
|
progress={progress}
|
||||||
onClick={() => setShowCode((prev) => !prev)}
|
onClick={() => setShowCode((prev) => !prev)}
|
||||||
inProgressText="Analyzing"
|
inProgressText={localize('com_ui_analyzing')}
|
||||||
finishedText="Finished analyzing"
|
finishedText={localize('com_ui_analyzing_finished')}
|
||||||
hasInput={!!code.length}
|
hasInput={!!code.length}
|
||||||
|
isExpanded={showCode}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{showCode && (
|
{showCode && (
|
||||||
|
|
|
||||||
|
|
@ -50,11 +50,24 @@ const ContentParts = memo(
|
||||||
[attachments, messageAttachmentsMap, messageId],
|
[attachments, messageAttachmentsMap, messageId],
|
||||||
);
|
);
|
||||||
|
|
||||||
const hasReasoningParts = useMemo(
|
const hasReasoningParts = useMemo(() => {
|
||||||
() => content?.some((part) => part?.type === ContentTypes.THINK && part.think) ?? false,
|
const hasThinkPart = content?.some((part) => part?.type === ContentTypes.THINK) ?? false;
|
||||||
[content],
|
const allThinkPartsHaveContent =
|
||||||
);
|
content?.every((part) => {
|
||||||
|
if (part?.type !== ContentTypes.THINK) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof part.think === 'string') {
|
||||||
|
const cleanedContent = part.think.replace(/<\/?think>/g, '').trim();
|
||||||
|
return cleanedContent.length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}) ?? false;
|
||||||
|
|
||||||
|
return hasThinkPart && allThinkPartsHaveContent;
|
||||||
|
}, [content]);
|
||||||
if (!content) {
|
if (!content) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,14 +9,14 @@ const Files = ({ message }: { message?: TMessage }) => {
|
||||||
}, [message?.files]);
|
}, [message?.files]);
|
||||||
|
|
||||||
const otherFiles = useMemo(() => {
|
const otherFiles = useMemo(() => {
|
||||||
return message?.files?.filter((file) => !file.type?.startsWith('image/')) || [];
|
return message?.files?.filter((file) => !(file.type?.startsWith('image/') === true)) || [];
|
||||||
}, [message?.files]);
|
}, [message?.files]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{otherFiles.length > 0 &&
|
{otherFiles.length > 0 &&
|
||||||
otherFiles.map((file) => <FileContainer key={file.file_id} file={file as TFile} />)}
|
otherFiles.map((file) => <FileContainer key={file.file_id} file={file as TFile} />)}
|
||||||
{imageFiles &&
|
{imageFiles.length > 0 &&
|
||||||
imageFiles.map((file) => (
|
imageFiles.map((file) => (
|
||||||
<Image
|
<Image
|
||||||
key={file.file_id}
|
key={file.file_id}
|
||||||
|
|
|
||||||
|
|
@ -9,12 +9,12 @@ const scaleImage = ({
|
||||||
originalHeight,
|
originalHeight,
|
||||||
containerRef,
|
containerRef,
|
||||||
}: {
|
}: {
|
||||||
originalWidth: number;
|
originalWidth?: number;
|
||||||
originalHeight: number;
|
originalHeight?: number;
|
||||||
containerRef: React.RefObject<HTMLDivElement>;
|
containerRef: React.RefObject<HTMLDivElement>;
|
||||||
}) => {
|
}) => {
|
||||||
const containerWidth = containerRef.current?.offsetWidth ?? 0;
|
const containerWidth = containerRef.current?.offsetWidth ?? 0;
|
||||||
if (containerWidth === 0 || originalWidth === undefined || originalHeight === undefined) {
|
if (containerWidth === 0 || originalWidth == null || originalHeight == null) {
|
||||||
return { width: 'auto', height: 'auto' };
|
return { width: 'auto', height: 'auto' };
|
||||||
}
|
}
|
||||||
const aspectRatio = originalWidth / originalHeight;
|
const aspectRatio = originalWidth / originalHeight;
|
||||||
|
|
@ -35,8 +35,8 @@ const Image = ({
|
||||||
height: number;
|
height: number;
|
||||||
width: number;
|
width: number;
|
||||||
placeholderDimensions?: {
|
placeholderDimensions?: {
|
||||||
height: string;
|
height?: string;
|
||||||
width: string;
|
width?: string;
|
||||||
};
|
};
|
||||||
}) => {
|
}) => {
|
||||||
const [isLoaded, setIsLoaded] = useState(false);
|
const [isLoaded, setIsLoaded] = useState(false);
|
||||||
|
|
@ -47,8 +47,8 @@ const Image = ({
|
||||||
const { width: scaledWidth, height: scaledHeight } = useMemo(
|
const { width: scaledWidth, height: scaledHeight } = useMemo(
|
||||||
() =>
|
() =>
|
||||||
scaleImage({
|
scaleImage({
|
||||||
originalWidth: Number(placeholderDimensions?.width?.split('px')[0]) ?? width,
|
originalWidth: Number(placeholderDimensions?.width?.split('px')[0] ?? width),
|
||||||
originalHeight: Number(placeholderDimensions?.height?.split('px')[0]) ?? height,
|
originalHeight: Number(placeholderDimensions?.height?.split('px')[0] ?? height),
|
||||||
containerRef,
|
containerRef,
|
||||||
}),
|
}),
|
||||||
[placeholderDimensions, height, width],
|
[placeholderDimensions, height, width],
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ export default function InProgressCall({
|
||||||
progress: number;
|
progress: number;
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}) {
|
}) {
|
||||||
if ((!isSubmitting && progress < 1) || error) {
|
if ((!isSubmitting && progress < 1) || error === true) {
|
||||||
return <CancelledIcon />;
|
return <CancelledIcon />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -159,7 +159,9 @@ const MessageContent = ({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{thinkingContent && <Thinking key={`thinking-${messageId}`}>{thinkingContent}</Thinking>}
|
{thinkingContent.length > 0 && (
|
||||||
|
<Thinking key={`thinking-${messageId}`}>{thinkingContent}</Thinking>
|
||||||
|
)}
|
||||||
<DisplayMessage
|
<DisplayMessage
|
||||||
key={`display-${messageId}`}
|
key={`display-${messageId}`}
|
||||||
showCursor={showRegularCursor}
|
showCursor={showRegularCursor}
|
||||||
|
|
|
||||||
|
|
@ -81,6 +81,8 @@ const Part = memo(({ part, isSubmitting, attachments, showCursor, isCreatedByUse
|
||||||
initialProgress={toolCall.progress ?? 0.1}
|
initialProgress={toolCall.progress ?? 0.1}
|
||||||
isSubmitting={isSubmitting}
|
isSubmitting={isSubmitting}
|
||||||
attachments={attachments}
|
attachments={attachments}
|
||||||
|
auth={toolCall.auth}
|
||||||
|
expires_at={toolCall.expires_at}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
} else if (toolCall.type === ToolCallTypes.CODE_INTERPRETER) {
|
} else if (toolCall.type === ToolCallTypes.CODE_INTERPRETER) {
|
||||||
|
|
|
||||||
|
|
@ -4,10 +4,10 @@ import type { TAttachment } from 'librechat-data-provider';
|
||||||
import ProgressText from '~/components/Chat/Messages/Content/ProgressText';
|
import ProgressText from '~/components/Chat/Messages/Content/ProgressText';
|
||||||
import FinishedIcon from '~/components/Chat/Messages/Content/FinishedIcon';
|
import FinishedIcon from '~/components/Chat/Messages/Content/FinishedIcon';
|
||||||
import MarkdownLite from '~/components/Chat/Messages/Content/MarkdownLite';
|
import MarkdownLite from '~/components/Chat/Messages/Content/MarkdownLite';
|
||||||
|
import { useProgress, useLocalize } from '~/hooks';
|
||||||
import { CodeInProgress } from './CodeProgress';
|
import { CodeInProgress } from './CodeProgress';
|
||||||
import Attachment from './Attachment';
|
import Attachment from './Attachment';
|
||||||
import LogContent from './LogContent';
|
import LogContent from './LogContent';
|
||||||
import { useProgress } from '~/hooks';
|
|
||||||
import store from '~/store';
|
import store from '~/store';
|
||||||
|
|
||||||
interface ParsedArgs {
|
interface ParsedArgs {
|
||||||
|
|
@ -36,6 +36,9 @@ export function useParseArgs(args: string): ParsedArgs {
|
||||||
}, [args]);
|
}, [args]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const radius = 56.08695652173913;
|
||||||
|
const circumference = 2 * Math.PI * radius;
|
||||||
|
|
||||||
export default function ExecuteCode({
|
export default function ExecuteCode({
|
||||||
initialProgress = 0.1,
|
initialProgress = 0.1,
|
||||||
args,
|
args,
|
||||||
|
|
@ -49,14 +52,12 @@ export default function ExecuteCode({
|
||||||
isSubmitting: boolean;
|
isSubmitting: boolean;
|
||||||
attachments?: TAttachment[];
|
attachments?: TAttachment[];
|
||||||
}) {
|
}) {
|
||||||
|
const localize = useLocalize();
|
||||||
const showAnalysisCode = useRecoilValue(store.showCode);
|
const showAnalysisCode = useRecoilValue(store.showCode);
|
||||||
const [showCode, setShowCode] = useState(showAnalysisCode);
|
const [showCode, setShowCode] = useState(showAnalysisCode);
|
||||||
|
|
||||||
const { lang, code } = useParseArgs(args);
|
const { lang, code } = useParseArgs(args);
|
||||||
const progress = useProgress(initialProgress);
|
const progress = useProgress(initialProgress);
|
||||||
|
|
||||||
const radius = 56.08695652173913;
|
|
||||||
const circumference = 2 * Math.PI * radius;
|
|
||||||
const offset = circumference - progress * circumference;
|
const offset = circumference - progress * circumference;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -78,9 +79,10 @@ export default function ExecuteCode({
|
||||||
<ProgressText
|
<ProgressText
|
||||||
progress={progress}
|
progress={progress}
|
||||||
onClick={() => setShowCode((prev) => !prev)}
|
onClick={() => setShowCode((prev) => !prev)}
|
||||||
inProgressText="Analyzing"
|
inProgressText={localize('com_ui_analyzing')}
|
||||||
finishedText="Finished analyzing"
|
finishedText={localize('com_ui_analyzing_finished')}
|
||||||
hasInput={!!code.length}
|
hasInput={!!code.length}
|
||||||
|
isExpanded={showCode}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{showCode && (
|
{showCode && (
|
||||||
|
|
@ -105,9 +107,7 @@ export default function ExecuteCode({
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{attachments?.map((attachment, index) => (
|
{attachments?.map((attachment, index) => <Attachment attachment={attachment} key={index} />)}
|
||||||
<Attachment attachment={attachment} key={index} />
|
|
||||||
))}
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -64,7 +64,7 @@ const LogContent: React.FC<LogContentProps> = ({ output = '', renderImages, atta
|
||||||
}
|
}
|
||||||
|
|
||||||
// const expirationText = expiresAt
|
// const expirationText = expiresAt
|
||||||
// ? ` ${localize('com_download_expires', format(expiresAt, 'MM/dd/yy HH:mm'))}`
|
// ? ` ${localize('com_download_expires', { 0: format(expiresAt, 'MM/dd/yy HH:mm') })}`
|
||||||
// : ` ${localize('com_click_to_download')}`;
|
// : ` ${localize('com_click_to_download')}`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
|
|
@ -11,9 +11,16 @@ type ReasoningProps = {
|
||||||
const Reasoning = memo(({ reasoning }: ReasoningProps) => {
|
const Reasoning = memo(({ reasoning }: ReasoningProps) => {
|
||||||
const { isExpanded, nextType } = useMessageContext();
|
const { isExpanded, nextType } = useMessageContext();
|
||||||
const reasoningText = useMemo(() => {
|
const reasoningText = useMemo(() => {
|
||||||
return reasoning.replace(/^<think>\s*/, '').replace(/\s*<\/think>$/, '');
|
return reasoning
|
||||||
|
.replace(/^<think>\s*/, '')
|
||||||
|
.replace(/\s*<\/think>$/, '')
|
||||||
|
.trim();
|
||||||
}, [reasoning]);
|
}, [reasoning]);
|
||||||
|
|
||||||
|
if (!reasoningText) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
|
|
|
||||||
|
|
@ -39,16 +39,21 @@ export default function ProgressText({
|
||||||
onClick,
|
onClick,
|
||||||
inProgressText,
|
inProgressText,
|
||||||
finishedText,
|
finishedText,
|
||||||
|
authText,
|
||||||
hasInput = true,
|
hasInput = true,
|
||||||
popover = false,
|
popover = false,
|
||||||
|
isExpanded = false,
|
||||||
}: {
|
}: {
|
||||||
progress: number;
|
progress: number;
|
||||||
onClick: () => void;
|
onClick?: () => void;
|
||||||
inProgressText: string;
|
inProgressText: string;
|
||||||
finishedText: string;
|
finishedText: string;
|
||||||
|
authText?: string;
|
||||||
hasInput?: boolean;
|
hasInput?: boolean;
|
||||||
popover?: boolean;
|
popover?: boolean;
|
||||||
|
isExpanded?: boolean;
|
||||||
}) {
|
}) {
|
||||||
|
const text = progress < 1 ? (authText ?? inProgressText) : finishedText;
|
||||||
return (
|
return (
|
||||||
<Wrapper popover={popover}>
|
<Wrapper popover={popover}>
|
||||||
<button
|
<button
|
||||||
|
|
@ -57,8 +62,14 @@ export default function ProgressText({
|
||||||
disabled={!hasInput}
|
disabled={!hasInput}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
>
|
>
|
||||||
{progress < 1 ? inProgressText : finishedText}
|
{text}
|
||||||
<svg width="16" height="17" viewBox="0 0 16 17" fill="none">
|
<svg
|
||||||
|
width="16"
|
||||||
|
height="17"
|
||||||
|
viewBox="0 0 16 17"
|
||||||
|
fill="none"
|
||||||
|
className={isExpanded ? 'rotate-180' : 'rotate-0'}
|
||||||
|
>
|
||||||
<path
|
<path
|
||||||
className={hasInput ? '' : 'stroke-transparent'}
|
className={hasInput ? '' : 'stroke-transparent'}
|
||||||
d="M11.3346 7.83203L8.00131 11.1654L4.66797 7.83203"
|
d="M11.3346 7.83203L8.00131 11.1654L4.66797 7.83203"
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import * as Popover from '@radix-ui/react-popover';
|
import * as Popover from '@radix-ui/react-popover';
|
||||||
|
import { ShieldCheck, TriangleAlert } from 'lucide-react';
|
||||||
import { actionDelimiter, actionDomainSeparator, Constants } from 'librechat-data-provider';
|
import { actionDelimiter, actionDomainSeparator, Constants } from 'librechat-data-provider';
|
||||||
import type { TAttachment } from 'librechat-data-provider';
|
import type { TAttachment } from 'librechat-data-provider';
|
||||||
import useLocalize from '~/hooks/useLocalize';
|
import useLocalize from '~/hooks/useLocalize';
|
||||||
|
|
@ -14,6 +15,9 @@ import WrenchIcon from './WrenchIcon';
|
||||||
import { useProgress } from '~/hooks';
|
import { useProgress } from '~/hooks';
|
||||||
import { logger } from '~/utils';
|
import { logger } from '~/utils';
|
||||||
|
|
||||||
|
const radius = 56.08695652173913;
|
||||||
|
const circumference = 2 * Math.PI * radius;
|
||||||
|
|
||||||
export default function ToolCall({
|
export default function ToolCall({
|
||||||
initialProgress = 0.1,
|
initialProgress = 0.1,
|
||||||
isSubmitting,
|
isSubmitting,
|
||||||
|
|
@ -21,6 +25,7 @@ export default function ToolCall({
|
||||||
args: _args = '',
|
args: _args = '',
|
||||||
output,
|
output,
|
||||||
attachments,
|
attachments,
|
||||||
|
auth,
|
||||||
}: {
|
}: {
|
||||||
initialProgress: number;
|
initialProgress: number;
|
||||||
isSubmitting: boolean;
|
isSubmitting: boolean;
|
||||||
|
|
@ -28,13 +33,10 @@ export default function ToolCall({
|
||||||
args: string | Record<string, unknown>;
|
args: string | Record<string, unknown>;
|
||||||
output?: string | null;
|
output?: string | null;
|
||||||
attachments?: TAttachment[];
|
attachments?: TAttachment[];
|
||||||
|
auth?: string;
|
||||||
|
expires_at?: number;
|
||||||
}) {
|
}) {
|
||||||
const localize = useLocalize();
|
const localize = useLocalize();
|
||||||
const progress = useProgress(initialProgress);
|
|
||||||
const radius = 56.08695652173913;
|
|
||||||
const circumference = 2 * Math.PI * radius;
|
|
||||||
const offset = circumference - progress * circumference;
|
|
||||||
|
|
||||||
const { function_name, domain, isMCPToolCall } = useMemo(() => {
|
const { function_name, domain, isMCPToolCall } = useMemo(() => {
|
||||||
if (typeof name !== 'string') {
|
if (typeof name !== 'string') {
|
||||||
return { function_name: '', domain: null, isMCPToolCall: false };
|
return { function_name: '', domain: null, isMCPToolCall: false };
|
||||||
|
|
@ -83,8 +85,37 @@ export default function ToolCall({
|
||||||
[args, output],
|
[args, output],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const authDomain = useMemo(() => {
|
||||||
|
const authURL = auth ?? '';
|
||||||
|
if (!authURL) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const url = new URL(authURL);
|
||||||
|
return url.hostname;
|
||||||
|
} catch (e) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}, [auth]);
|
||||||
|
|
||||||
|
const progress = useProgress(error === true ? 1 : initialProgress);
|
||||||
|
const cancelled = (!isSubmitting && progress < 1) || error === true;
|
||||||
|
const offset = circumference - progress * circumference;
|
||||||
|
|
||||||
const renderIcon = () => {
|
const renderIcon = () => {
|
||||||
if (progress < 1) {
|
if (progress < 1 && authDomain.length > 0) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="absolute left-0 top-0 flex h-full w-full items-center justify-center rounded-full bg-transparent text-text-secondary"
|
||||||
|
style={{ opacity: 1, transform: 'none' }}
|
||||||
|
data-projection-id="849"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<ShieldCheck />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
} else if (progress < 1) {
|
||||||
return (
|
return (
|
||||||
<InProgressCall progress={progress} isSubmitting={isSubmitting} error={error}>
|
<InProgressCall progress={progress} isSubmitting={isSubmitting} error={error}>
|
||||||
<div
|
<div
|
||||||
|
|
@ -101,43 +132,67 @@ export default function ToolCall({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return error === true ? <CancelledIcon /> : <FinishedIcon />;
|
return cancelled ? <CancelledIcon /> : <FinishedIcon />;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getFinishedText = () => {
|
const getFinishedText = () => {
|
||||||
|
if (cancelled) {
|
||||||
|
return localize('com_ui_error');
|
||||||
|
}
|
||||||
if (isMCPToolCall === true) {
|
if (isMCPToolCall === true) {
|
||||||
return localize('com_assistants_completed_function', function_name);
|
return localize('com_assistants_completed_function', { 0: function_name });
|
||||||
}
|
}
|
||||||
if (domain != null && domain && domain.length !== Constants.ENCODED_DOMAIN_LENGTH) {
|
if (domain != null && domain && domain.length !== Constants.ENCODED_DOMAIN_LENGTH) {
|
||||||
return localize('com_assistants_completed_action', domain);
|
return localize('com_assistants_completed_action', { 0: domain });
|
||||||
}
|
}
|
||||||
return localize('com_assistants_completed_function', function_name);
|
return localize('com_assistants_completed_function', { 0: function_name });
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover.Root>
|
<Popover.Root>
|
||||||
<div className="my-2.5 flex items-center gap-2.5">
|
<div className="my-2.5 flex flex-wrap items-center gap-2.5">
|
||||||
<div className="relative h-5 w-5 shrink-0">{renderIcon()}</div>
|
<div className="flex w-full items-center gap-2.5">
|
||||||
<ProgressText
|
<div className="relative h-5 w-5 shrink-0">{renderIcon()}</div>
|
||||||
progress={progress}
|
<ProgressText
|
||||||
onClick={() => ({})}
|
progress={cancelled ? 1 : progress}
|
||||||
inProgressText={localize('com_assistants_running_action')}
|
inProgressText={localize('com_assistants_running_action')}
|
||||||
finishedText={getFinishedText()}
|
authText={
|
||||||
hasInput={hasInfo}
|
!cancelled && authDomain.length > 0 ? localize('com_ui_requires_auth') : undefined
|
||||||
popover={true}
|
}
|
||||||
/>
|
finishedText={getFinishedText()}
|
||||||
{hasInfo && (
|
hasInput={hasInfo}
|
||||||
<ToolPopover
|
popover={true}
|
||||||
input={args ?? ''}
|
|
||||||
output={output}
|
|
||||||
domain={domain ?? ''}
|
|
||||||
function_name={function_name}
|
|
||||||
/>
|
/>
|
||||||
|
{hasInfo && (
|
||||||
|
<ToolPopover
|
||||||
|
input={args ?? ''}
|
||||||
|
output={output}
|
||||||
|
domain={authDomain || (domain ?? '')}
|
||||||
|
function_name={function_name}
|
||||||
|
pendingAuth={authDomain.length > 0 && !cancelled && progress < 1}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{auth != null && auth && progress < 1 && !cancelled && (
|
||||||
|
<div className="flex w-full flex-col gap-2.5">
|
||||||
|
<div className="mb-1 mt-2">
|
||||||
|
<a
|
||||||
|
className="inline-flex items-center justify-center gap-2 rounded-3xl bg-surface-tertiary px-4 py-2 text-sm font-medium hover:bg-surface-hover"
|
||||||
|
href={auth}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
{localize('com_ui_sign_in_to_domain', { 0: authDomain })}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<p className="flex items-center text-xs text-text-secondary">
|
||||||
|
<TriangleAlert className="mr-1.5 inline-block h-4 w-4" />
|
||||||
|
{localize('com_assistants_allow_sites_you_trust')}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{attachments?.map((attachment, index) => (
|
{attachments?.map((attachment, index) => <Attachment attachment={attachment} key={index} />)}
|
||||||
<Attachment attachment={attachment} key={index} />
|
|
||||||
))}
|
|
||||||
</Popover.Root>
|
</Popover.Root>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,13 +4,15 @@ import useLocalize from '~/hooks/useLocalize';
|
||||||
export default function ToolPopover({
|
export default function ToolPopover({
|
||||||
input,
|
input,
|
||||||
output,
|
output,
|
||||||
function_name,
|
|
||||||
domain,
|
domain,
|
||||||
|
function_name,
|
||||||
|
pendingAuth,
|
||||||
}: {
|
}: {
|
||||||
input: string;
|
input: string;
|
||||||
function_name: string;
|
function_name: string;
|
||||||
output?: string | null;
|
output?: string | null;
|
||||||
domain?: string;
|
domain?: string;
|
||||||
|
pendingAuth?: boolean;
|
||||||
}) {
|
}) {
|
||||||
const localize = useLocalize();
|
const localize = useLocalize();
|
||||||
const formatText = (text: string) => {
|
const formatText = (text: string) => {
|
||||||
|
|
@ -21,6 +23,17 @@ export default function ToolPopover({
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let title =
|
||||||
|
domain != null && domain
|
||||||
|
? localize('com_assistants_domain_info', { 0: domain })
|
||||||
|
: localize('com_assistants_function_use', { 0: function_name });
|
||||||
|
if (pendingAuth === true) {
|
||||||
|
title =
|
||||||
|
domain != null && domain
|
||||||
|
? localize('com_assistants_action_attempt', { 0: domain })
|
||||||
|
: localize('com_assistants_attempt_info');
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover.Portal>
|
<Popover.Portal>
|
||||||
<Popover.Content
|
<Popover.Content
|
||||||
|
|
@ -28,27 +41,23 @@ export default function ToolPopover({
|
||||||
align="start"
|
align="start"
|
||||||
sideOffset={12}
|
sideOffset={12}
|
||||||
alignOffset={-5}
|
alignOffset={-5}
|
||||||
className="w-18 min-w-[180px] max-w-sm rounded-lg bg-white dark:bg-gray-900"
|
className="w-18 min-w-[180px] max-w-sm rounded-lg bg-surface-primary px-1"
|
||||||
>
|
>
|
||||||
<div tabIndex={-1}>
|
<div tabIndex={-1}>
|
||||||
<div className="bg-token-surface-primary max-w-sm rounded-md p-2 shadow-[0_0_24px_0_rgba(0,0,0,0.05),inset_0_0.5px_0_0_rgba(0,0,0,0.05),0_2px_8px_0_rgba(0,0,0,0.05)]">
|
<div className="bg-token-surface-primary max-w-sm rounded-md p-2 shadow-[0_0_24px_0_rgba(0,0,0,0.05),inset_0_0.5px_0_0_rgba(0,0,0,0.05),0_2px_8px_0_rgba(0,0,0,0.05)]">
|
||||||
<div className="mb-2 text-sm font-medium dark:text-gray-100">
|
<div className="mb-2 text-sm font-medium text-text-primary">{title}</div>
|
||||||
{domain != null && domain
|
|
||||||
? localize('com_assistants_domain_info', domain)
|
|
||||||
: localize('com_assistants_function_use', function_name)}
|
|
||||||
</div>
|
|
||||||
<div className="bg-token-surface-secondary text-token-text-primary dark rounded-md text-xs">
|
<div className="bg-token-surface-secondary text-token-text-primary dark rounded-md text-xs">
|
||||||
<div className="max-h-32 overflow-y-auto rounded-md p-2 dark:bg-gray-700">
|
<div className="max-h-32 overflow-y-auto rounded-md bg-surface-tertiary p-2">
|
||||||
<code className="!whitespace-pre-wrap ">{formatText(input)}</code>
|
<code className="!whitespace-pre-wrap ">{formatText(input)}</code>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{output != null && output && (
|
{output != null && output && (
|
||||||
<>
|
<>
|
||||||
<div className="mb-2 mt-2 text-sm font-medium dark:text-gray-100">
|
<div className="mb-2 mt-2 text-sm font-medium text-text-primary">
|
||||||
{localize('com_ui_result')}
|
{localize('com_ui_result')}
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-token-surface-secondary text-token-text-primary dark rounded-md text-xs">
|
<div className="bg-token-surface-secondary text-token-text-primary dark rounded-md text-xs">
|
||||||
<div className="max-h-32 overflow-y-auto rounded-md p-2 dark:bg-gray-700">
|
<div className="max-h-32 overflow-y-auto rounded-md bg-surface-tertiary p-2">
|
||||||
<code className="!whitespace-pre-wrap ">{formatText(output)}</code>
|
<code className="!whitespace-pre-wrap ">{formatText(output)}</code>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,37 +1,19 @@
|
||||||
import { useRecoilValue } from 'recoil';
|
import { useRecoilValue } from 'recoil';
|
||||||
import { useEffect, useMemo } from 'react';
|
import { useEffect, useMemo } from 'react';
|
||||||
import { FileSources, LocalStorageKeys, getConfigDefaults } from 'librechat-data-provider';
|
import { FileSources, LocalStorageKeys } from 'librechat-data-provider';
|
||||||
import type { ExtendedFile } from '~/common';
|
import type { ExtendedFile } from '~/common';
|
||||||
import { useDeleteFilesMutation, useGetStartupConfig } from '~/data-provider';
|
import { useDeleteFilesMutation } from '~/data-provider';
|
||||||
import DragDropWrapper from '~/components/Chat/Input/Files/DragDropWrapper';
|
import DragDropWrapper from '~/components/Chat/Input/Files/DragDropWrapper';
|
||||||
import Artifacts from '~/components/Artifacts/Artifacts';
|
import Artifacts from '~/components/Artifacts/Artifacts';
|
||||||
import { SidePanel } from '~/components/SidePanel';
|
import { SidePanelGroup } from '~/components/SidePanel';
|
||||||
import { useSetFilesToDelete } from '~/hooks';
|
import { useSetFilesToDelete } from '~/hooks';
|
||||||
import { EditorProvider } from '~/Providers';
|
import { EditorProvider } from '~/Providers';
|
||||||
import store from '~/store';
|
import store from '~/store';
|
||||||
|
|
||||||
const defaultInterface = getConfigDefaults().interface;
|
export default function Presentation({ children }: { children: React.ReactNode }) {
|
||||||
|
|
||||||
export default function Presentation({
|
|
||||||
children,
|
|
||||||
useSidePanel = false,
|
|
||||||
panel,
|
|
||||||
}: {
|
|
||||||
children: React.ReactNode;
|
|
||||||
panel?: React.ReactNode;
|
|
||||||
useSidePanel?: boolean;
|
|
||||||
}) {
|
|
||||||
const { data: startupConfig } = useGetStartupConfig();
|
|
||||||
const artifacts = useRecoilValue(store.artifactsState);
|
const artifacts = useRecoilValue(store.artifactsState);
|
||||||
const codeArtifacts = useRecoilValue(store.codeArtifacts);
|
|
||||||
const hideSidePanel = useRecoilValue(store.hideSidePanel);
|
|
||||||
const artifactsVisible = useRecoilValue(store.artifactsVisible);
|
const artifactsVisible = useRecoilValue(store.artifactsVisible);
|
||||||
|
|
||||||
const interfaceConfig = useMemo(
|
|
||||||
() => startupConfig?.interface ?? defaultInterface,
|
|
||||||
[startupConfig],
|
|
||||||
);
|
|
||||||
|
|
||||||
const setFilesToDelete = useSetFilesToDelete();
|
const setFilesToDelete = useSetFilesToDelete();
|
||||||
|
|
||||||
const { mutateAsync } = useDeleteFilesMutation({
|
const { mutateAsync } = useDeleteFilesMutation({
|
||||||
|
|
@ -83,35 +65,24 @@ export default function Presentation({
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
if (useSidePanel && !hideSidePanel && interfaceConfig.sidePanel === true) {
|
|
||||||
return (
|
|
||||||
<DragDropWrapper className="relative flex w-full grow overflow-hidden bg-presentation">
|
|
||||||
<SidePanel
|
|
||||||
defaultLayout={defaultLayout}
|
|
||||||
defaultCollapsed={defaultCollapsed}
|
|
||||||
fullPanelCollapse={fullCollapse}
|
|
||||||
artifacts={
|
|
||||||
artifactsVisible === true &&
|
|
||||||
codeArtifacts === true &&
|
|
||||||
Object.keys(artifacts ?? {}).length > 0 ? (
|
|
||||||
<EditorProvider>
|
|
||||||
<Artifacts />
|
|
||||||
</EditorProvider>
|
|
||||||
) : null
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<main className="flex h-full flex-col overflow-y-auto" role="main">
|
|
||||||
{children}
|
|
||||||
</main>
|
|
||||||
</SidePanel>
|
|
||||||
</DragDropWrapper>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DragDropWrapper className="relative flex w-full grow overflow-hidden bg-presentation">
|
<DragDropWrapper className="relative flex w-full grow overflow-hidden bg-presentation">
|
||||||
{layout()}
|
<SidePanelGroup
|
||||||
{panel != null && panel}
|
defaultLayout={defaultLayout}
|
||||||
|
fullPanelCollapse={fullCollapse}
|
||||||
|
defaultCollapsed={defaultCollapsed}
|
||||||
|
artifacts={
|
||||||
|
artifactsVisible === true && Object.keys(artifacts ?? {}).length > 0 ? (
|
||||||
|
<EditorProvider>
|
||||||
|
<Artifacts />
|
||||||
|
</EditorProvider>
|
||||||
|
) : null
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<main className="flex h-full flex-col overflow-y-auto" role="main">
|
||||||
|
{children}
|
||||||
|
</main>
|
||||||
|
</SidePanelGroup>
|
||||||
</DragDropWrapper>
|
</DragDropWrapper>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,14 @@
|
||||||
import { TPromptGroup } from 'librechat-data-provider';
|
import { TPromptGroup } from 'librechat-data-provider';
|
||||||
import CategoryIcon from '~/components/Prompts/Groups/CategoryIcon';
|
import CategoryIcon from '~/components/Prompts/Groups/CategoryIcon';
|
||||||
|
|
||||||
export default function PromptCard({ promptGroup }: { promptGroup: TPromptGroup }) {
|
export default function PromptCard({ promptGroup }: { promptGroup?: TPromptGroup }) {
|
||||||
return (
|
return (
|
||||||
<div className="hover:bg-token-main-surface-secondary relative flex w-40 cursor-pointer flex-col gap-2 rounded-2xl border px-3 pb-4 pt-3 text-start align-top text-[15px] shadow-[0_0_2px_0_rgba(0,0,0,0.05),0_4px_6px_0_rgba(0,0,0,0.02)] transition-colors duration-300 ease-in-out fade-in hover:bg-slate-100 dark:border-gray-600 dark:hover:bg-gray-700">
|
<div className="hover:bg-token-main-surface-secondary relative flex w-40 cursor-pointer flex-col gap-2 rounded-2xl border px-3 pb-4 pt-3 text-start align-top text-[15px] shadow-[0_0_2px_0_rgba(0,0,0,0.05),0_4px_6px_0_rgba(0,0,0,0.02)] transition-colors duration-300 ease-in-out fade-in hover:bg-slate-100 dark:border-gray-600 dark:hover:bg-gray-700">
|
||||||
<div className="">
|
<div className="">
|
||||||
<CategoryIcon className="size-4" category={promptGroup.category || ''} />
|
<CategoryIcon className="size-4" category={promptGroup?.category ?? ''} />
|
||||||
</div>
|
</div>
|
||||||
<p className="break-word line-clamp-3 text-balance text-gray-600 dark:text-gray-400">
|
<p className="break-word line-clamp-3 text-balance text-gray-600 dark:text-gray-400">
|
||||||
{promptGroup?.oneliner || promptGroup?.productionPrompt?.prompt}
|
{(promptGroup?.oneliner ?? '') || promptGroup?.productionPrompt?.prompt}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
import { useMemo, memo } from 'react';
|
import { useMemo, memo } from 'react';
|
||||||
import { parseISO, isToday } from 'date-fns';
|
import { parseISO, isToday } from 'date-fns';
|
||||||
import { TConversation } from 'librechat-data-provider';
|
import { TConversation } from 'librechat-data-provider';
|
||||||
|
import { useLocalize, TranslationKeys } from '~/hooks';
|
||||||
import { groupConversationsByDate } from '~/utils';
|
import { groupConversationsByDate } from '~/utils';
|
||||||
import { useLocalize } from '~/hooks';
|
|
||||||
import Convo from './Convo';
|
import Convo from './Convo';
|
||||||
|
|
||||||
const Conversations = ({
|
const Conversations = ({
|
||||||
|
|
@ -41,8 +41,7 @@ const Conversations = ({
|
||||||
paddingLeft: '10px',
|
paddingLeft: '10px',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* eslint-disable-next-line @typescript-eslint/strict-boolean-expressions */}
|
{localize(groupName as TranslationKeys) || groupName}
|
||||||
{localize(groupName) || groupName}
|
|
||||||
</div>
|
</div>
|
||||||
{convos.map((convo, i) => (
|
{convos.map((convo, i) => (
|
||||||
<Convo
|
<Convo
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ import {
|
||||||
HoverCardContent,
|
HoverCardContent,
|
||||||
} from '~/components/ui';
|
} from '~/components/ui';
|
||||||
import OptionHover from '~/components/SidePanel/Parameters/OptionHover';
|
import OptionHover from '~/components/SidePanel/Parameters/OptionHover';
|
||||||
import { useLocalize, useNavigateToConvo } from '~/hooks';
|
import { TranslationKeys, useLocalize, useNavigateToConvo } from '~/hooks';
|
||||||
import { useForkConvoMutation } from '~/data-provider';
|
import { useForkConvoMutation } from '~/data-provider';
|
||||||
import { useToastContext } from '~/Providers';
|
import { useToastContext } from '~/Providers';
|
||||||
import { ESide } from '~/common';
|
import { ESide } from '~/common';
|
||||||
|
|
@ -201,7 +201,7 @@ export default function Fork({
|
||||||
align="center"
|
align="center"
|
||||||
>
|
>
|
||||||
<div className="flex h-6 w-full items-center justify-center text-sm dark:text-gray-200">
|
<div className="flex h-6 w-full items-center justify-center text-sm dark:text-gray-200">
|
||||||
{localize(activeSetting)}
|
{localize(activeSetting as TranslationKeys)}
|
||||||
<HoverCard openDelay={50}>
|
<HoverCard openDelay={50}>
|
||||||
<HoverCardTrigger asChild>
|
<HoverCardTrigger asChild>
|
||||||
<InfoIcon className="ml-auto flex h-4 w-4 gap-2 text-gray-500 dark:text-white/50" />
|
<InfoIcon className="ml-auto flex h-4 w-4 gap-2 text-gray-500 dark:text-white/50" />
|
||||||
|
|
@ -216,7 +216,9 @@ export default function Fork({
|
||||||
<span>{localize('com_ui_fork_info_1')}</span>
|
<span>{localize('com_ui_fork_info_1')}</span>
|
||||||
<span>{localize('com_ui_fork_info_2')}</span>
|
<span>{localize('com_ui_fork_info_2')}</span>
|
||||||
<span>
|
<span>
|
||||||
{localize('com_ui_fork_info_3', localize('com_ui_fork_split_target'))}
|
{localize('com_ui_fork_info_3', {
|
||||||
|
0: localize('com_ui_fork_split_target'),
|
||||||
|
})}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</HoverCardContent>
|
</HoverCardContent>
|
||||||
|
|
@ -233,7 +235,7 @@ export default function Fork({
|
||||||
hoverTitle={
|
hoverTitle={
|
||||||
<>
|
<>
|
||||||
<GitCommit className="h-5 w-5 rotate-90" />
|
<GitCommit className="h-5 w-5 rotate-90" />
|
||||||
{localize(optionLabels[ForkOptions.DIRECT_PATH])}
|
{localize(optionLabels[ForkOptions.DIRECT_PATH] as TranslationKeys)}
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
hoverDescription={localize('com_ui_fork_info_visible')}
|
hoverDescription={localize('com_ui_fork_info_visible')}
|
||||||
|
|
@ -251,7 +253,7 @@ export default function Fork({
|
||||||
hoverTitle={
|
hoverTitle={
|
||||||
<>
|
<>
|
||||||
<GitBranchPlus className="h-4 w-4 rotate-180" />
|
<GitBranchPlus className="h-4 w-4 rotate-180" />
|
||||||
{localize(optionLabels[ForkOptions.INCLUDE_BRANCHES])}
|
{localize(optionLabels[ForkOptions.INCLUDE_BRANCHES] as TranslationKeys)}
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
hoverDescription={localize('com_ui_fork_info_branches')}
|
hoverDescription={localize('com_ui_fork_info_branches')}
|
||||||
|
|
@ -269,9 +271,9 @@ export default function Fork({
|
||||||
hoverTitle={
|
hoverTitle={
|
||||||
<>
|
<>
|
||||||
<ListTree className="h-5 w-5" />
|
<ListTree className="h-5 w-5" />
|
||||||
{`${localize(optionLabels[ForkOptions.TARGET_LEVEL])} (${localize(
|
{`${localize(
|
||||||
'com_endpoint_default',
|
optionLabels[ForkOptions.TARGET_LEVEL] as TranslationKeys,
|
||||||
)})`}
|
)} (${localize('com_endpoint_default')})`}
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
hoverDescription={localize('com_ui_fork_info_target')}
|
hoverDescription={localize('com_ui_fork_info_target')}
|
||||||
|
|
|
||||||
|
|
@ -69,6 +69,7 @@ const MinimalIcon: React.FC<IconProps> = (props) => {
|
||||||
<div
|
<div
|
||||||
data-testid="convo-icon"
|
data-testid="convo-icon"
|
||||||
title={name}
|
title={name}
|
||||||
|
aria-hidden="true"
|
||||||
style={{
|
style={{
|
||||||
width: size,
|
width: size,
|
||||||
height: size,
|
height: size,
|
||||||
|
|
|
||||||
|
|
@ -121,7 +121,7 @@ export default function Settings({
|
||||||
<Label htmlFor="temp-int" className="text-left text-sm font-medium">
|
<Label htmlFor="temp-int" className="text-left text-sm font-medium">
|
||||||
{localize('com_endpoint_temperature')}{' '}
|
{localize('com_endpoint_temperature')}{' '}
|
||||||
<small className="opacity-40">
|
<small className="opacity-40">
|
||||||
({localize('com_endpoint_default_with_num', '1')})
|
({localize('com_endpoint_default_with_num', { 0: '1' })})
|
||||||
</small>
|
</small>
|
||||||
</Label>
|
</Label>
|
||||||
<InputNumber
|
<InputNumber
|
||||||
|
|
|
||||||
|
|
@ -27,12 +27,12 @@ export default function Settings({ conversation, setOption, models, readonly }:
|
||||||
conversation ?? {};
|
conversation ?? {};
|
||||||
|
|
||||||
const currentList = useMemo(
|
const currentList = useMemo(
|
||||||
() => Object.values(assistantListMap?.[endpoint ?? ''] ?? {}) as Assistant[],
|
() => Object.values(assistantListMap[endpoint ?? ''] ?? {}) as Assistant[],
|
||||||
[assistantListMap, endpoint],
|
[assistantListMap, endpoint],
|
||||||
);
|
);
|
||||||
|
|
||||||
const assistants = useMemo(() => {
|
const assistants = useMemo(() => {
|
||||||
const currentAssistants = (currentList ?? []).map(({ id, name }) => ({
|
const currentAssistants = currentList.map(({ id, name }) => ({
|
||||||
label: name,
|
label: name,
|
||||||
value: id,
|
value: id,
|
||||||
}));
|
}));
|
||||||
|
|
@ -52,8 +52,8 @@ export default function Settings({ conversation, setOption, models, readonly }:
|
||||||
});
|
});
|
||||||
|
|
||||||
const activeAssistant = useMemo(() => {
|
const activeAssistant = useMemo(() => {
|
||||||
if (assistant_id) {
|
if (assistant_id != null && assistant_id) {
|
||||||
return assistantListMap[endpoint ?? '']?.[assistant_id];
|
return assistantListMap[endpoint ?? '']?.[assistant_id] as Assistant | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
|
|
@ -70,11 +70,13 @@ export default function Settings({ conversation, setOption, models, readonly }:
|
||||||
}, [models, activeAssistant, localize]);
|
}, [models, activeAssistant, localize]);
|
||||||
|
|
||||||
const [assistantValue, setAssistantValue] = useState<Option>(
|
const [assistantValue, setAssistantValue] = useState<Option>(
|
||||||
activeAssistant ? { label: activeAssistant.name, value: activeAssistant.id } : defaultOption,
|
activeAssistant != null
|
||||||
|
? { label: activeAssistant.name ?? '', value: activeAssistant.id }
|
||||||
|
: defaultOption,
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (assistantValue && assistantValue.value === '') {
|
if (assistantValue.value === '') {
|
||||||
setOption('presetOverride')({
|
setOption('presetOverride')({
|
||||||
assistant_id: assistantValue.value,
|
assistant_id: assistantValue.value,
|
||||||
} as Partial<TPreset>);
|
} as Partial<TPreset>);
|
||||||
|
|
@ -95,7 +97,7 @@ export default function Settings({ conversation, setOption, models, readonly }:
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const assistant = assistantListMap[endpoint ?? '']?.[value];
|
const assistant = assistantListMap[endpoint ?? '']?.[value] as Assistant | null;
|
||||||
if (!assistant) {
|
if (!assistant) {
|
||||||
setAssistantValue(defaultOption);
|
setAssistantValue(defaultOption);
|
||||||
return;
|
return;
|
||||||
|
|
@ -103,7 +105,7 @@ export default function Settings({ conversation, setOption, models, readonly }:
|
||||||
|
|
||||||
setAssistantValue({
|
setAssistantValue({
|
||||||
label: assistant.name ?? '',
|
label: assistant.name ?? '',
|
||||||
value: assistant.id ?? '',
|
value: assistant.id || '',
|
||||||
});
|
});
|
||||||
setOption('assistant_id')(assistant.id);
|
setOption('assistant_id')(assistant.id);
|
||||||
if (assistant.model) {
|
if (assistant.model) {
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,7 @@ function Examples({ readonly, examples, setExample, addExample, removeExample }:
|
||||||
<TextareaAutosize
|
<TextareaAutosize
|
||||||
id={`input-${idx}`}
|
id={`input-${idx}`}
|
||||||
disabled={readonly}
|
disabled={readonly}
|
||||||
value={example?.input?.content || ''}
|
value={example.input.content || ''}
|
||||||
onChange={(e) => setExample(idx, 'input', e.target.value ?? null)}
|
onChange={(e) => setExample(idx, 'input', e.target.value ?? null)}
|
||||||
placeholder="Set example input. Example is ignored if empty."
|
placeholder="Set example input. Example is ignored if empty."
|
||||||
className={cn(
|
className={cn(
|
||||||
|
|
@ -62,7 +62,7 @@ function Examples({ readonly, examples, setExample, addExample, removeExample }:
|
||||||
<TextareaAutosize
|
<TextareaAutosize
|
||||||
id={`output-${idx}`}
|
id={`output-${idx}`}
|
||||||
disabled={readonly}
|
disabled={readonly}
|
||||||
value={example?.output?.content || ''}
|
value={example.output.content || ''}
|
||||||
onChange={(e) => setExample(idx, 'output', e.target.value ?? null)}
|
onChange={(e) => setExample(idx, 'output', e.target.value ?? null)}
|
||||||
placeholder={'Set example output. Example is ignored if empty.'}
|
placeholder={'Set example output. Example is ignored if empty.'}
|
||||||
className={cn(
|
className={cn(
|
||||||
|
|
|
||||||
|
|
@ -180,7 +180,7 @@ export default function Settings({ conversation, setOption, models, readonly }:
|
||||||
<Label htmlFor="top-p-int" className="text-left text-sm font-medium">
|
<Label htmlFor="top-p-int" className="text-left text-sm font-medium">
|
||||||
{localize('com_endpoint_top_p')}{' '}
|
{localize('com_endpoint_top_p')}{' '}
|
||||||
<small className="opacity-40">
|
<small className="opacity-40">
|
||||||
({localize('com_endpoint_default_with_num', google.topP.default + '')})
|
({localize('com_endpoint_default_with_num', { 0: google.topP.default + '' })})
|
||||||
</small>
|
</small>
|
||||||
</Label>
|
</Label>
|
||||||
<InputNumber
|
<InputNumber
|
||||||
|
|
@ -221,7 +221,7 @@ export default function Settings({ conversation, setOption, models, readonly }:
|
||||||
<Label htmlFor="top-k-int" className="text-left text-sm font-medium">
|
<Label htmlFor="top-k-int" className="text-left text-sm font-medium">
|
||||||
{localize('com_endpoint_top_k')}{' '}
|
{localize('com_endpoint_top_k')}{' '}
|
||||||
<small className="opacity-40">
|
<small className="opacity-40">
|
||||||
({localize('com_endpoint_default_with_num', google.topK.default + '')})
|
({localize('com_endpoint_default_with_num',{ 0: google.topK.default + '' })})
|
||||||
</small>
|
</small>
|
||||||
</Label>
|
</Label>
|
||||||
<InputNumber
|
<InputNumber
|
||||||
|
|
@ -261,7 +261,7 @@ export default function Settings({ conversation, setOption, models, readonly }:
|
||||||
<Label htmlFor="max-tokens-int" className="text-left text-sm font-medium">
|
<Label htmlFor="max-tokens-int" className="text-left text-sm font-medium">
|
||||||
{localize('com_endpoint_max_output_tokens')}{' '}
|
{localize('com_endpoint_max_output_tokens')}{' '}
|
||||||
<small className="opacity-40">
|
<small className="opacity-40">
|
||||||
({localize('com_endpoint_default_with_num', google.maxOutputTokens.default + '')})
|
({localize('com_endpoint_default_with_num', { 0: google.maxOutputTokens.default + '' })})
|
||||||
</small>
|
</small>
|
||||||
</Label>
|
</Label>
|
||||||
<InputNumber
|
<InputNumber
|
||||||
|
|
|
||||||
|
|
@ -226,7 +226,7 @@ export default function Settings({
|
||||||
<Label htmlFor="temp-int" className="text-left text-sm font-medium">
|
<Label htmlFor="temp-int" className="text-left text-sm font-medium">
|
||||||
{localize('com_endpoint_temperature')}{' '}
|
{localize('com_endpoint_temperature')}{' '}
|
||||||
<small className="opacity-40">
|
<small className="opacity-40">
|
||||||
({localize('com_endpoint_default_with_num', '0.8')})
|
({localize('com_endpoint_default_with_num', { 0: '0.8' })})
|
||||||
</small>
|
</small>
|
||||||
</Label>
|
</Label>
|
||||||
<InputNumber
|
<InputNumber
|
||||||
|
|
@ -266,7 +266,7 @@ export default function Settings({
|
||||||
<Label htmlFor="top-p-int" className="text-left text-sm font-medium">
|
<Label htmlFor="top-p-int" className="text-left text-sm font-medium">
|
||||||
{localize('com_endpoint_top_p')}{' '}
|
{localize('com_endpoint_top_p')}{' '}
|
||||||
<small className="opacity-40">
|
<small className="opacity-40">
|
||||||
({localize('com_endpoint_default_with_num', '1')})
|
({localize('com_endpoint_default_with_num', { 0: '1' })})
|
||||||
</small>
|
</small>
|
||||||
</Label>
|
</Label>
|
||||||
<InputNumber
|
<InputNumber
|
||||||
|
|
@ -307,7 +307,7 @@ export default function Settings({
|
||||||
<Label htmlFor="freq-penalty-int" className="text-left text-sm font-medium">
|
<Label htmlFor="freq-penalty-int" className="text-left text-sm font-medium">
|
||||||
{localize('com_endpoint_frequency_penalty')}{' '}
|
{localize('com_endpoint_frequency_penalty')}{' '}
|
||||||
<small className="opacity-40">
|
<small className="opacity-40">
|
||||||
({localize('com_endpoint_default_with_num', '0')})
|
({localize('com_endpoint_default_with_num', { 0: '0' })})
|
||||||
</small>
|
</small>
|
||||||
</Label>
|
</Label>
|
||||||
<InputNumber
|
<InputNumber
|
||||||
|
|
@ -348,7 +348,7 @@ export default function Settings({
|
||||||
<Label htmlFor="pres-penalty-int" className="text-left text-sm font-medium">
|
<Label htmlFor="pres-penalty-int" className="text-left text-sm font-medium">
|
||||||
{localize('com_endpoint_presence_penalty')}{' '}
|
{localize('com_endpoint_presence_penalty')}{' '}
|
||||||
<small className="opacity-40">
|
<small className="opacity-40">
|
||||||
({localize('com_endpoint_default_with_num', '0')})
|
({localize('com_endpoint_default_with_num', { 0: '0' })})
|
||||||
</small>
|
</small>
|
||||||
</Label>
|
</Label>
|
||||||
<InputNumber
|
<InputNumber
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ const FileDashboardView = () => {
|
||||||
return (
|
return (
|
||||||
<div className="bg-[#f9f9f9] p-0 lg:p-7">
|
<div className="bg-[#f9f9f9] p-0 lg:p-7">
|
||||||
<div className="ml-3 mt-3 flex flex-row justify-between">
|
<div className="ml-3 mt-3 flex flex-row justify-between">
|
||||||
{params?.vectorStoreId && (
|
{params.vectorStoreId && (
|
||||||
<Button
|
<Button
|
||||||
className="block lg:hidden"
|
className="block lg:hidden"
|
||||||
variant={'outline'}
|
variant={'outline'}
|
||||||
|
|
|
||||||
|
|
@ -244,9 +244,10 @@ export default function DataTableFile<TData, TValue>({
|
||||||
<div className="ml-4 mr-4 mt-4 flex h-auto items-center justify-end space-x-2 py-4 sm:ml-0 sm:mr-0 sm:h-0">
|
<div className="ml-4 mr-4 mt-4 flex h-auto items-center justify-end space-x-2 py-4 sm:ml-0 sm:mr-0 sm:h-0">
|
||||||
<div className="text-muted-foreground ml-2 flex-1 text-sm">
|
<div className="text-muted-foreground ml-2 flex-1 text-sm">
|
||||||
{localize(
|
{localize(
|
||||||
'com_files_number_selected',
|
'com_files_number_selected', {
|
||||||
`${table.getFilteredSelectedRowModel().rows.length}`,
|
0: `${table.getFilteredSelectedRowModel().rows.length}`,
|
||||||
`${table.getFilteredRowModel().rows.length}`,
|
1: `${table.getFilteredRowModel().rows.length}`,
|
||||||
|
},
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
|
|
|
||||||
|
|
@ -2,16 +2,18 @@ import React from 'react';
|
||||||
import FileSidePanel from './FileList/FileSidePanel';
|
import FileSidePanel from './FileList/FileSidePanel';
|
||||||
import { Outlet, useNavigate, useParams } from 'react-router-dom';
|
import { Outlet, useNavigate, useParams } from 'react-router-dom';
|
||||||
import FilesSectionSelector from './FilesSectionSelector';
|
import FilesSectionSelector from './FilesSectionSelector';
|
||||||
|
import { useLocalize } from '~/hooks';
|
||||||
import { Button } from '../ui';
|
import { Button } from '../ui';
|
||||||
|
|
||||||
export default function FilesListView() {
|
export default function FilesListView() {
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const localize = useLocalize();
|
||||||
return (
|
return (
|
||||||
<div className="bg-[#f9f9f9] p-0 lg:p-7">
|
<div className="bg-[#f9f9f9] p-0 lg:p-7">
|
||||||
<div className="m-4 flex w-full flex-row justify-between md:m-2">
|
<div className="m-4 flex w-full flex-row justify-between md:m-2">
|
||||||
<FilesSectionSelector />
|
<FilesSectionSelector />
|
||||||
{params?.fileId && (
|
{params.fileId != null && params.fileId && (
|
||||||
<Button
|
<Button
|
||||||
className="block lg:hidden"
|
className="block lg:hidden"
|
||||||
variant={'outline'}
|
variant={'outline'}
|
||||||
|
|
@ -20,21 +22,21 @@ export default function FilesListView() {
|
||||||
navigate('/d/files');
|
navigate('/d/files');
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Go back
|
{localize('com_ui_go_back')}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex w-full flex-row divide-x">
|
<div className="flex w-full flex-row divide-x">
|
||||||
<div
|
<div
|
||||||
className={`mr-2 w-full xl:w-1/3 ${
|
className={`mr-2 w-full xl:w-1/3 ${
|
||||||
params.fileId ? 'hidden w-1/2 lg:block lg:w-1/2' : 'md:w-full'
|
params.fileId != null && params.fileId ? 'hidden w-1/2 lg:block lg:w-1/2' : 'md:w-full'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<FileSidePanel />
|
<FileSidePanel />
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className={`h-[85vh] w-full overflow-y-auto xl:w-2/3 ${
|
className={`h-[85vh] w-full overflow-y-auto xl:w-2/3 ${
|
||||||
params.fileId ? 'lg:w-1/2' : 'hidden md:w-1/2 lg:block'
|
params.fileId != null && params.fileId ? 'lg:w-1/2' : 'hidden md:w-1/2 lg:block'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<Outlet />
|
<Outlet />
|
||||||
|
|
|
||||||
|
|
@ -178,7 +178,7 @@ export default function VectorStorePreview() {
|
||||||
<Clock3 className="text-base text-gray-500 md:text-lg lg:text-xl" />
|
<Clock3 className="text-base text-gray-500 md:text-lg lg:text-xl" />
|
||||||
Created At
|
Created At
|
||||||
</span>
|
</span>
|
||||||
<span className="w-1/2 text-gray-500 md:w-3/5">{vectorStore.createdAt?.toString()}</span>
|
<span className="w-1/2 text-gray-500 md:w-3/5">{vectorStore.createdAt.toString()}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,7 @@ export default function ModelSelect({
|
||||||
}
|
}
|
||||||
|
|
||||||
const { endpoint: _endpoint, endpointType } = conversation;
|
const { endpoint: _endpoint, endpointType } = conversation;
|
||||||
const models = modelsQuery?.data?.[_endpoint] ?? [];
|
const models = modelsQuery.data?.[_endpoint] ?? [];
|
||||||
const endpoint = endpointType ?? _endpoint;
|
const endpoint = endpointType ?? _endpoint;
|
||||||
|
|
||||||
const OptionComponent = multiChatOptions[endpoint];
|
const OptionComponent = multiChatOptions[endpoint];
|
||||||
|
|
|
||||||
|
|
@ -40,13 +40,15 @@ export const TemporaryChat = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="sticky bottom-0 border-none bg-surface-tertiary px-6 py-4 ">
|
<div className="sticky bottom-0 mt-auto w-full border-none bg-surface-tertiary px-6 py-4">
|
||||||
<div className="flex items-center">
|
<div className="flex items-center justify-between">
|
||||||
<div className={cn('flex flex-1 items-center gap-2', isActiveConvo && 'opacity-40')}>
|
<div className={cn('flex items-center gap-2', isActiveConvo && 'opacity-40')}>
|
||||||
<MessageCircleDashed className="icon-sm" />
|
<MessageCircleDashed className="icon-sm" aria-hidden="true" />
|
||||||
<span className="text-sm text-text-primary">{localize('com_ui_temporary_chat')}</span>
|
<span className="truncate text-sm text-text-primary">
|
||||||
|
{localize('com_ui_temporary_chat')}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="ml-auto flex items-center">
|
<div className="flex flex-shrink-0 items-center">
|
||||||
<Switch
|
<Switch
|
||||||
id="temporary-chat-switch"
|
id="temporary-chat-switch"
|
||||||
checked={isTemporary}
|
checked={isTemporary}
|
||||||
|
|
|
||||||
|
|
@ -47,11 +47,11 @@ const errorMessages = {
|
||||||
[ErrorTypes.NO_SYSTEM_MESSAGES]: `com_error_${ErrorTypes.NO_SYSTEM_MESSAGES}`,
|
[ErrorTypes.NO_SYSTEM_MESSAGES]: `com_error_${ErrorTypes.NO_SYSTEM_MESSAGES}`,
|
||||||
[ErrorTypes.EXPIRED_USER_KEY]: (json: TExpiredKey, localize: LocalizeFunction) => {
|
[ErrorTypes.EXPIRED_USER_KEY]: (json: TExpiredKey, localize: LocalizeFunction) => {
|
||||||
const { expiredAt, endpoint } = json;
|
const { expiredAt, endpoint } = json;
|
||||||
return localize('com_error_expired_user_key', endpoint, expiredAt);
|
return localize('com_error_expired_user_key', { 0: endpoint, 1: expiredAt });
|
||||||
},
|
},
|
||||||
[ErrorTypes.INPUT_LENGTH]: (json: TGenericError, localize: LocalizeFunction) => {
|
[ErrorTypes.INPUT_LENGTH]: (json: TGenericError, localize: LocalizeFunction) => {
|
||||||
const { info } = json;
|
const { info } = json;
|
||||||
return localize('com_error_input_length', info);
|
return localize('com_error_input_length', { 0: info });
|
||||||
},
|
},
|
||||||
[ErrorTypes.GOOGLE_ERROR]: (json: TGenericError) => {
|
[ErrorTypes.GOOGLE_ERROR]: (json: TGenericError) => {
|
||||||
const { info } = json;
|
const { info } = json;
|
||||||
|
|
|
||||||
|
|
@ -35,9 +35,9 @@ const BookmarkNav: FC<BookmarkNavProps> = ({ tags, setTags, isSmallScreen }: Boo
|
||||||
<div className="h-7 w-7 flex-shrink-0">
|
<div className="h-7 w-7 flex-shrink-0">
|
||||||
<div className="relative flex h-full items-center justify-center rounded-full border border-border-medium bg-surface-primary-alt text-text-primary">
|
<div className="relative flex h-full items-center justify-center rounded-full border border-border-medium bg-surface-primary-alt text-text-primary">
|
||||||
{tags.length > 0 ? (
|
{tags.length > 0 ? (
|
||||||
<BookmarkFilledIcon className="h-4 w-4" />
|
<BookmarkFilledIcon className="h-4 w-4" aria-hidden="true" />
|
||||||
) : (
|
) : (
|
||||||
<BookmarkIcon className="h-4 w-4" />
|
<BookmarkIcon className="h-4 w-4" aria-hidden="true" />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -70,7 +70,7 @@ function Avatar() {
|
||||||
const megabytes =
|
const megabytes =
|
||||||
fileConfig.avatarSizeLimit != null ? formatBytes(fileConfig.avatarSizeLimit) : 2;
|
fileConfig.avatarSizeLimit != null ? formatBytes(fileConfig.avatarSizeLimit) : 2;
|
||||||
showToast({
|
showToast({
|
||||||
message: localize('com_ui_upload_invalid_var', megabytes + ''),
|
message: localize('com_ui_upload_invalid_var', { 0: megabytes + '' }),
|
||||||
status: 'error',
|
status: 'error',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -41,7 +41,7 @@ export const RevokeKeysButton = ({
|
||||||
|
|
||||||
const dialogTitle = all
|
const dialogTitle = all
|
||||||
? localize('com_ui_revoke_keys')
|
? localize('com_ui_revoke_keys')
|
||||||
: localize('com_ui_revoke_key_endpoint', endpoint);
|
: localize('com_ui_revoke_key_endpoint', { 0: endpoint });
|
||||||
|
|
||||||
const dialogMessage = all
|
const dialogMessage = all
|
||||||
? localize('com_ui_revoke_keys_confirm')
|
? localize('com_ui_revoke_keys_confirm')
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ export default function DecibelSelector() {
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div>{localize('com_nav_db_sensitivity')}</div>
|
<div>{localize('com_nav_db_sensitivity')}</div>
|
||||||
<div className="w-2" />
|
<div className="w-2" />
|
||||||
<small className="opacity-40">({localize('com_endpoint_default_with_num', '-45')})</small>
|
<small className="opacity-40">({localize('com_endpoint_default_with_num', { 0: '-45' })})</small>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<Slider
|
<Slider
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ export default function DecibelSelector() {
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div>{localize('com_nav_playback_rate')}</div>
|
<div>{localize('com_nav_playback_rate')}</div>
|
||||||
<div className="w-2" />
|
<div className="w-2" />
|
||||||
<small className="opacity-40">({localize('com_endpoint_default_with_num', '1')})</small>
|
<small className="opacity-40">({localize('com_endpoint_default_with_num', { 0: '1' })})</small>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<Slider
|
<Slider
|
||||||
|
|
|
||||||
|
|
@ -153,7 +153,7 @@ const AdminSettings = () => {
|
||||||
variant="outline"
|
variant="outline"
|
||||||
className="mr-2 h-10 w-fit gap-1 border transition-all dark:bg-transparent dark:hover:bg-surface-tertiary sm:m-0"
|
className="mr-2 h-10 w-fit gap-1 border transition-all dark:bg-transparent dark:hover:bg-surface-tertiary sm:m-0"
|
||||||
>
|
>
|
||||||
<ShieldEllipsis className="cursor-pointer" />
|
<ShieldEllipsis className="cursor-pointer" aria-hidden="true" />
|
||||||
<span className="hidden sm:flex">{localize('com_ui_admin')}</span>
|
<span className="hidden sm:flex">{localize('com_ui_admin')}</span>
|
||||||
</Button>
|
</Button>
|
||||||
</OGDialogTrigger>
|
</OGDialogTrigger>
|
||||||
|
|
|
||||||
|
|
@ -46,7 +46,7 @@ const Command = ({
|
||||||
return (
|
return (
|
||||||
<div className="rounded-xl border border-border-light">
|
<div className="rounded-xl border border-border-light">
|
||||||
<h3 className="flex h-10 items-center gap-1 pl-4 text-sm text-text-secondary">
|
<h3 className="flex h-10 items-center gap-1 pl-4 text-sm text-text-secondary">
|
||||||
<SquareSlash className="icon-sm" />
|
<SquareSlash className="icon-sm" aria-hidden="true" />
|
||||||
<Input
|
<Input
|
||||||
type="text"
|
type="text"
|
||||||
tabIndex={tabIndex}
|
tabIndex={tabIndex}
|
||||||
|
|
|
||||||
|
|
@ -42,7 +42,7 @@ const DeleteVersion = ({
|
||||||
htmlFor="dialog-delete-confirm-prompt"
|
htmlFor="dialog-delete-confirm-prompt"
|
||||||
className="text-left text-sm font-medium"
|
className="text-left text-sm font-medium"
|
||||||
>
|
>
|
||||||
{localize('com_ui_delete_confirm_prompt_version_var', name)}
|
{localize('com_ui_delete_confirm_prompt_version_var', { 0: name })}
|
||||||
</Label>
|
</Label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -43,7 +43,7 @@ const Description = ({
|
||||||
return (
|
return (
|
||||||
<div className="rounded-xl border border-border-light">
|
<div className="rounded-xl border border-border-light">
|
||||||
<h3 className="flex h-10 items-center gap-1 pl-4 text-sm text-text-secondary">
|
<h3 className="flex h-10 items-center gap-1 pl-4 text-sm text-text-secondary">
|
||||||
<Info className="icon-sm" />
|
<Info className="icon-sm" aria-hidden="true" />
|
||||||
<Input
|
<Input
|
||||||
type="text"
|
type="text"
|
||||||
tabIndex={tabIndex}
|
tabIndex={tabIndex}
|
||||||
|
|
|
||||||
|
|
@ -48,5 +48,5 @@ export default function CategoryIcon({
|
||||||
if (!IconComponent) {
|
if (!IconComponent) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return <IconComponent className={cn(colorClass, className)} />;
|
return <IconComponent className={cn(colorClass, className)} aria-hidden="true" />;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -30,17 +30,13 @@ export default function GroupSidePanel({
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'mr-2 flex h-auto w-auto min-w-72 flex-col gap-2 lg:w-1/4 xl:w-1/4',
|
'mr-2 flex h-auto w-auto min-w-72 flex-col gap-2 lg:w-1/4 xl:w-1/4',
|
||||||
isDetailView && isSmallerScreen ? 'hidden' : '',
|
isDetailView === true && isSmallerScreen ? 'hidden' : '',
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
<div className="flex-grow overflow-y-auto">
|
<div className="flex-grow overflow-y-auto">
|
||||||
<List
|
<List groups={promptGroups} isChatRoute={isChatRoute} isLoading={!!groupsQuery.isLoading} />
|
||||||
groups={promptGroups}
|
|
||||||
isChatRoute={isChatRoute}
|
|
||||||
isLoading={!!groupsQuery.isLoading}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<PanelNavigation
|
<PanelNavigation
|
||||||
nextPage={nextPage}
|
nextPage={nextPage}
|
||||||
|
|
|
||||||
|
|
@ -169,7 +169,7 @@ export default function VariableForm({
|
||||||
return (
|
return (
|
||||||
<InputCombobox
|
<InputCombobox
|
||||||
options={field.config.options || []}
|
options={field.config.options || []}
|
||||||
placeholder={localize('com_ui_enter_var', field.config.variable)}
|
placeholder={localize('com_ui_enter_var', { 0: field.config.variable })}
|
||||||
className={cn(
|
className={cn(
|
||||||
defaultTextProps,
|
defaultTextProps,
|
||||||
'rounded px-3 py-2 focus:bg-surface-tertiary',
|
'rounded px-3 py-2 focus:bg-surface-tertiary',
|
||||||
|
|
@ -192,7 +192,7 @@ export default function VariableForm({
|
||||||
defaultTextProps,
|
defaultTextProps,
|
||||||
'rounded px-3 py-2 focus:bg-surface-tertiary',
|
'rounded px-3 py-2 focus:bg-surface-tertiary',
|
||||||
)}
|
)}
|
||||||
placeholder={localize('com_ui_enter_var', field.config.variable)}
|
placeholder={localize('com_ui_enter_var', { 0: field.config.variable })}
|
||||||
maxRows={8}
|
maxRows={8}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -70,7 +70,7 @@ const PromptForm = () => {
|
||||||
|
|
||||||
const selectedPrompt = useMemo(
|
const selectedPrompt = useMemo(
|
||||||
() => (prompts.length > 0 ? prompts[selectionIndex] : undefined),
|
() => (prompts.length > 0 ? prompts[selectionIndex] : undefined),
|
||||||
[prompts, /* eslint-disable-line react-hooks/exhaustive-deps */ selectionIndex],
|
[prompts, selectionIndex],
|
||||||
);
|
);
|
||||||
|
|
||||||
const { groupsQuery } = useOutletContext<ReturnType<typeof usePromptGroupsNav>>();
|
const { groupsQuery } = useOutletContext<ReturnType<typeof usePromptGroupsNav>>();
|
||||||
|
|
@ -102,7 +102,7 @@ const PromptForm = () => {
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
onSuccess(data) {
|
onSuccess(data) {
|
||||||
if (alwaysMakeProd && data.prompt._id && data.prompt.groupId) {
|
if (alwaysMakeProd && data.prompt._id != null && data.prompt._id && data.prompt.groupId) {
|
||||||
makeProductionMutation.mutate({
|
makeProductionMutation.mutate({
|
||||||
id: data.prompt._id,
|
id: data.prompt._id,
|
||||||
groupId: data.prompt.groupId,
|
groupId: data.prompt.groupId,
|
||||||
|
|
@ -336,7 +336,7 @@ const PromptForm = () => {
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
className="h-10 w-10 border border-border-light p-0 lg:hidden"
|
className="h-10 w-10 border border-border-light p-0 lg:hidden"
|
||||||
onClick={() => setShowSidePanel(true)}
|
onClick={() => setShowSidePanel(true)}
|
||||||
aria-label={localize('com_ui_open_menu')}
|
aria-label={localize('com_endpoint_open_menu')}
|
||||||
>
|
>
|
||||||
<Menu className="size-5" />
|
<Menu className="size-5" />
|
||||||
</Button>
|
</Button>
|
||||||
|
|
@ -382,8 +382,8 @@ const PromptForm = () => {
|
||||||
onClick={() => setShowSidePanel(false)}
|
onClick={() => setShowSidePanel(false)}
|
||||||
aria-hidden={!showSidePanel}
|
aria-hidden={!showSidePanel}
|
||||||
tabIndex={showSidePanel ? 0 : -1}
|
tabIndex={showSidePanel ? 0 : -1}
|
||||||
|
aria-label={localize('com_ui_close_menu')}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className="absolute inset-y-0 right-0 z-50 lg:hidden"
|
className="absolute inset-y-0 right-0 z-50 lg:hidden"
|
||||||
style={{
|
style={{
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,7 @@ const PromptVariables = ({
|
||||||
return (
|
return (
|
||||||
<div className="rounded-xl border border-border-light bg-transparent p-4 shadow-md ">
|
<div className="rounded-xl border border-border-light bg-transparent p-4 shadow-md ">
|
||||||
<h3 className="flex items-center gap-2 py-2 text-lg font-semibold text-text-primary">
|
<h3 className="flex items-center gap-2 py-2 text-lg font-semibold text-text-primary">
|
||||||
<Variable className="icon-sm" />
|
<Variable className="icon-sm" aria-hidden="true" />
|
||||||
{localize('com_ui_variables')}
|
{localize('com_ui_variables')}
|
||||||
</h3>
|
</h3>
|
||||||
<div className="flex flex-col space-y-4">
|
<div className="flex flex-col space-y-4">
|
||||||
|
|
|
||||||
|
|
@ -111,12 +111,12 @@ const VersionCard = ({
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
aria-selected={isSelected}
|
aria-selected={isSelected}
|
||||||
role="tab"
|
role="tab"
|
||||||
aria-label={localize('com_ui_version_var', `${totalVersions - index}`)}
|
aria-label={localize('com_ui_version_var', { 0: `${totalVersions - index}` })}
|
||||||
>
|
>
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<div className="flex items-start justify-between lg:flex-col xl:flex-row">
|
<div className="flex items-start justify-between lg:flex-col xl:flex-row">
|
||||||
<h3 className="font-bold text-text-primary">
|
<h3 className="font-bold text-text-primary">
|
||||||
{localize('com_ui_version_var', `${totalVersions - index}`)}
|
{localize('com_ui_version_var', { 0: `${totalVersions - index}` })}
|
||||||
</h3>
|
</h3>
|
||||||
<time className="text-xs text-text-secondary" dateTime={prompt.createdAt}>
|
<time className="text-xs text-text-secondary" dateTime={prompt.createdAt}>
|
||||||
{format(new Date(prompt.createdAt), 'yyyy-MM-dd HH:mm')}
|
{format(new Date(prompt.createdAt), 'yyyy-MM-dd HH:mm')}
|
||||||
|
|
|
||||||
|
|
@ -97,7 +97,7 @@ const SharePrompt = ({ group, disabled }: { group?: TPromptGroup; disabled: bool
|
||||||
</OGDialogTrigger>
|
</OGDialogTrigger>
|
||||||
<OGDialogContent className="w-11/12 max-w-lg" role="dialog" aria-labelledby="dialog-title">
|
<OGDialogContent className="w-11/12 max-w-lg" role="dialog" aria-labelledby="dialog-title">
|
||||||
<OGDialogTitle id="dialog-title" className="truncate pr-2" title={group.name}>
|
<OGDialogTitle id="dialog-title" className="truncate pr-2" title={group.name}>
|
||||||
{localize('com_ui_share_var', `"${group.name}"`)}
|
{localize('com_ui_share_var', { 0: `"${group.name}"` })}
|
||||||
</OGDialogTitle>
|
</OGDialogTitle>
|
||||||
<form className="p-2" onSubmit={handleSubmit(onSubmit)} aria-describedby="form-description">
|
<form className="p-2" onSubmit={handleSubmit(onSubmit)} aria-describedby="form-description">
|
||||||
<div id="form-description" className="sr-only">
|
<div id="form-description" className="sr-only">
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ export default function MessagesView({
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="flex flex-col pb-9 text-sm dark:bg-transparent">
|
<div className="flex flex-col pb-9 text-sm dark:bg-transparent">
|
||||||
{(_messagesTree && _messagesTree?.length == 0) || _messagesTree === null ? (
|
{(_messagesTree && _messagesTree.length == 0) || _messagesTree === null ? (
|
||||||
<div className="flex w-full items-center justify-center gap-1 bg-gray-50 p-3 text-sm text-gray-500 dark:border-gray-800/50 dark:bg-gray-800 dark:text-gray-300">
|
<div className="flex w-full items-center justify-center gap-1 bg-gray-50 p-3 text-sm text-gray-500 dark:border-gray-800/50 dark:bg-gray-800 dark:text-gray-300">
|
||||||
Nothing found
|
Nothing found
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { useRecoilState } from 'recoil';
|
import { useRecoilState } from 'recoil';
|
||||||
|
import type { TMessage } from 'librechat-data-provider';
|
||||||
import type { TMessageProps } from '~/common';
|
import type { TMessageProps } from '~/common';
|
||||||
// eslint-disable-next-line import/no-cycle
|
// eslint-disable-next-line import/no-cycle
|
||||||
import Message from './Message';
|
import Message from './Message';
|
||||||
|
|
@ -25,17 +26,16 @@ export default function MultiMessage({
|
||||||
}, [messagesTree?.length]);
|
}, [messagesTree?.length]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (messagesTree?.length && siblingIdx >= messagesTree?.length) {
|
if (messagesTree?.length != null && siblingIdx >= messagesTree.length) {
|
||||||
setSiblingIdx(0);
|
setSiblingIdx(0);
|
||||||
}
|
}
|
||||||
}, [siblingIdx, messagesTree?.length, setSiblingIdx]);
|
}, [siblingIdx, messagesTree?.length, setSiblingIdx]);
|
||||||
|
|
||||||
if (!(messagesTree && messagesTree?.length)) {
|
if (!(messagesTree && messagesTree.length)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const message = messagesTree[messagesTree.length - siblingIdx - 1];
|
const message = messagesTree[messagesTree.length - siblingIdx - 1] as TMessage | null;
|
||||||
|
|
||||||
if (!message) {
|
if (!message) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,296 +0,0 @@
|
||||||
import { useFormContext } from 'react-hook-form';
|
|
||||||
import * as RadioGroup from '@radix-ui/react-radio-group';
|
|
||||||
import * as DialogPrimitive from '@radix-ui/react-dialog';
|
|
||||||
import {
|
|
||||||
AuthTypeEnum,
|
|
||||||
AuthorizationTypeEnum,
|
|
||||||
TokenExchangeMethodEnum,
|
|
||||||
} from 'librechat-data-provider';
|
|
||||||
import { DialogContent } from '~/components/ui/';
|
|
||||||
|
|
||||||
export default function ActionsAuth({
|
|
||||||
setOpenAuthDialog,
|
|
||||||
}: {
|
|
||||||
setOpenAuthDialog: React.Dispatch<React.SetStateAction<boolean>>;
|
|
||||||
}) {
|
|
||||||
const { watch, setValue, trigger } = useFormContext();
|
|
||||||
const type = watch('type');
|
|
||||||
return (
|
|
||||||
<DialogContent
|
|
||||||
role="dialog"
|
|
||||||
id="radix-:rf5:"
|
|
||||||
aria-describedby="radix-:rf7:"
|
|
||||||
aria-labelledby="radix-:rf6:"
|
|
||||||
data-state="open"
|
|
||||||
className="left-1/2 col-auto col-start-2 row-auto row-start-2 w-full max-w-md -translate-x-1/2 rounded-xl bg-white pb-0 text-left shadow-xl transition-all dark:bg-gray-700 dark:text-gray-100"
|
|
||||||
tabIndex={-1}
|
|
||||||
style={{ pointerEvents: 'auto' }}
|
|
||||||
>
|
|
||||||
<div className="flex items-center justify-between border-b border-black/10 px-4 pb-4 pt-5 dark:border-white/10 sm:p-6">
|
|
||||||
<div className="flex">
|
|
||||||
<div className="flex items-center">
|
|
||||||
<div className="flex grow flex-col gap-1">
|
|
||||||
<h2
|
|
||||||
id="radix-:rf6:"
|
|
||||||
className="text-token-text-primary text-lg font-medium leading-6"
|
|
||||||
>
|
|
||||||
Authentication
|
|
||||||
</h2>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="p-4 sm:p-6 sm:pt-0">
|
|
||||||
<div className="mb-4">
|
|
||||||
<label className="mb-1 block text-sm font-medium">Authentication Type</label>
|
|
||||||
<RadioGroup.Root
|
|
||||||
defaultValue={AuthTypeEnum.None}
|
|
||||||
onValueChange={(value) => setValue('type', value)}
|
|
||||||
value={type}
|
|
||||||
role="radiogroup"
|
|
||||||
aria-required="false"
|
|
||||||
dir="ltr"
|
|
||||||
className="flex gap-4"
|
|
||||||
tabIndex={0}
|
|
||||||
style={{ outline: 'none' }}
|
|
||||||
>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<label htmlFor=":rf8:" className="flex cursor-pointer items-center gap-1">
|
|
||||||
<RadioGroup.Item
|
|
||||||
type="button"
|
|
||||||
role="radio"
|
|
||||||
value={AuthTypeEnum.None}
|
|
||||||
id=":rf8:"
|
|
||||||
className="mr-1 flex h-5 w-5 items-center justify-center rounded-full border border-gray-500 bg-white dark:border-gray-500 dark:bg-gray-500"
|
|
||||||
tabIndex={-1}
|
|
||||||
>
|
|
||||||
<RadioGroup.Indicator className="h-2 w-2 rounded-full bg-gray-950 dark:bg-white"></RadioGroup.Indicator>
|
|
||||||
</RadioGroup.Item>
|
|
||||||
None
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<label htmlFor=":rfa:" className="flex cursor-pointer items-center gap-1">
|
|
||||||
<RadioGroup.Item
|
|
||||||
type="button"
|
|
||||||
role="radio"
|
|
||||||
value={AuthTypeEnum.ServiceHttp}
|
|
||||||
id=":rfa:"
|
|
||||||
className="mr-1 flex h-5 w-5 items-center justify-center rounded-full border border-gray-500 bg-white dark:border-gray-500 dark:bg-gray-500"
|
|
||||||
tabIndex={0}
|
|
||||||
>
|
|
||||||
<RadioGroup.Indicator className="h-2 w-2 rounded-full bg-gray-950 dark:bg-white"></RadioGroup.Indicator>
|
|
||||||
</RadioGroup.Item>
|
|
||||||
API Key
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2 text-gray-500">
|
|
||||||
<label htmlFor=":rfc:" className="flex cursor-not-allowed items-center gap-1">
|
|
||||||
<RadioGroup.Item
|
|
||||||
type="button"
|
|
||||||
role="radio"
|
|
||||||
disabled={true}
|
|
||||||
value={AuthTypeEnum.OAuth}
|
|
||||||
id=":rfc:"
|
|
||||||
className="mr-1 flex h-5 w-5 cursor-not-allowed items-center justify-center rounded-full border border-gray-500 bg-gray-300 dark:border-gray-600 dark:bg-gray-700"
|
|
||||||
tabIndex={-1}
|
|
||||||
>
|
|
||||||
<RadioGroup.Indicator className="h-2 w-2 rounded-full bg-gray-950 dark:bg-white"></RadioGroup.Indicator>
|
|
||||||
</RadioGroup.Item>
|
|
||||||
OAuth
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</RadioGroup.Root>
|
|
||||||
</div>
|
|
||||||
{type === 'none' ? null : type === 'service_http' ? <ApiKey /> : <OAuth />}
|
|
||||||
{/* Cancel/Save */}
|
|
||||||
<div className="mt-5 flex flex-col gap-3 sm:mt-4 sm:flex-row-reverse">
|
|
||||||
<button
|
|
||||||
className="btn relative bg-green-500 text-white hover:bg-green-600 dark:hover:bg-green-600"
|
|
||||||
onClick={async () => {
|
|
||||||
const result = await trigger(undefined, { shouldFocus: true });
|
|
||||||
setValue('saved_auth_fields', result);
|
|
||||||
setOpenAuthDialog(!result);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div className="flex w-full items-center justify-center gap-2">Save</div>
|
|
||||||
</button>
|
|
||||||
<DialogPrimitive.Close className="btn btn-neutral relative">
|
|
||||||
<div className="flex w-full items-center justify-center gap-2">Cancel</div>
|
|
||||||
</DialogPrimitive.Close>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</DialogContent>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const ApiKey = () => {
|
|
||||||
const { register, watch, setValue } = useFormContext();
|
|
||||||
const authorization_type = watch('authorization_type');
|
|
||||||
const type = watch('type');
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<label className="mb-1 block text-sm font-medium">API Key</label>
|
|
||||||
<input
|
|
||||||
placeholder="<HIDDEN>"
|
|
||||||
type="password"
|
|
||||||
autoComplete="new-password"
|
|
||||||
className="border-token-border-medium mb-2 h-9 w-full resize-none overflow-y-auto rounded-lg border px-3 py-2 text-sm outline-none focus:ring-2 focus:ring-blue-400 dark:bg-gray-600"
|
|
||||||
{...register('api_key', { required: type === AuthTypeEnum.ServiceHttp })}
|
|
||||||
/>
|
|
||||||
<label className="mb-1 block text-sm font-medium">Auth Type</label>
|
|
||||||
<RadioGroup.Root
|
|
||||||
defaultValue={AuthorizationTypeEnum.Basic}
|
|
||||||
onValueChange={(value) => setValue('authorization_type', value)}
|
|
||||||
value={authorization_type}
|
|
||||||
role="radiogroup"
|
|
||||||
aria-required="true"
|
|
||||||
dir="ltr"
|
|
||||||
className="mb-2 flex gap-6 overflow-hidden rounded-lg"
|
|
||||||
tabIndex={0}
|
|
||||||
style={{ outline: 'none' }}
|
|
||||||
>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<label htmlFor=":rfu:" className="flex cursor-pointer items-center gap-1">
|
|
||||||
<RadioGroup.Item
|
|
||||||
type="button"
|
|
||||||
role="radio"
|
|
||||||
value={AuthorizationTypeEnum.Basic}
|
|
||||||
id=":rfu:"
|
|
||||||
className="mr-1 flex h-5 w-5 items-center justify-center rounded-full border border-gray-500 bg-white dark:border-gray-500 dark:bg-gray-500"
|
|
||||||
tabIndex={-1}
|
|
||||||
>
|
|
||||||
<RadioGroup.Indicator className="h-2 w-2 rounded-full bg-gray-950 dark:bg-white"></RadioGroup.Indicator>
|
|
||||||
</RadioGroup.Item>
|
|
||||||
Basic
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<label htmlFor=":rg0:" className="flex cursor-pointer items-center gap-1">
|
|
||||||
<RadioGroup.Item
|
|
||||||
type="button"
|
|
||||||
role="radio"
|
|
||||||
value={AuthorizationTypeEnum.Bearer}
|
|
||||||
id=":rg0:"
|
|
||||||
className="mr-1 flex h-5 w-5 items-center justify-center rounded-full border border-gray-500 bg-white dark:border-gray-500 dark:bg-gray-500"
|
|
||||||
tabIndex={-1}
|
|
||||||
>
|
|
||||||
<RadioGroup.Indicator className="h-2 w-2 rounded-full bg-gray-950 dark:bg-white"></RadioGroup.Indicator>
|
|
||||||
</RadioGroup.Item>
|
|
||||||
Bearer
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<label htmlFor=":rg2:" className="flex cursor-pointer items-center gap-1">
|
|
||||||
<RadioGroup.Item
|
|
||||||
type="button"
|
|
||||||
role="radio"
|
|
||||||
value={AuthorizationTypeEnum.Custom}
|
|
||||||
id=":rg2:"
|
|
||||||
className="mr-1 flex h-5 w-5 items-center justify-center rounded-full border border-gray-500 bg-white dark:border-gray-500 dark:bg-gray-500"
|
|
||||||
tabIndex={0}
|
|
||||||
>
|
|
||||||
<RadioGroup.Indicator className="h-2 w-2 rounded-full bg-gray-950 dark:bg-white"></RadioGroup.Indicator>
|
|
||||||
</RadioGroup.Item>
|
|
||||||
Custom
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</RadioGroup.Root>
|
|
||||||
{authorization_type === AuthorizationTypeEnum.Custom && (
|
|
||||||
<div className="mt-2">
|
|
||||||
<label className="mb-1 block text-sm font-medium">Custom Header Name</label>
|
|
||||||
<input
|
|
||||||
className="border-token-border-medium mb-2 h-9 w-full resize-none overflow-y-auto rounded-lg border px-3 py-2 text-sm outline-none focus:ring-2 focus:ring-blue-400 dark:bg-gray-600"
|
|
||||||
placeholder="X-Api-Key"
|
|
||||||
{...register('custom_auth_header', {
|
|
||||||
required: authorization_type === AuthorizationTypeEnum.Custom,
|
|
||||||
})}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const OAuth = () => {
|
|
||||||
const { register, watch, setValue } = useFormContext();
|
|
||||||
const token_exchange_method = watch('token_exchange_method');
|
|
||||||
const type = watch('type');
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<label className="mb-1 block text-sm font-medium">Client ID</label>
|
|
||||||
<input
|
|
||||||
placeholder="<HIDDEN>"
|
|
||||||
type="password"
|
|
||||||
autoComplete="off"
|
|
||||||
className="border-token-border-medium mb-2 h-9 w-full resize-none overflow-y-auto rounded-lg border px-3 py-2 text-sm outline-none focus:ring-2 focus:ring-blue-400 dark:bg-gray-800"
|
|
||||||
{...register('oauth_client_id', { required: type === AuthTypeEnum.OAuth })}
|
|
||||||
/>
|
|
||||||
<label className="mb-1 block text-sm font-medium">Client Secret</label>
|
|
||||||
<input
|
|
||||||
placeholder="<HIDDEN>"
|
|
||||||
type="password"
|
|
||||||
autoComplete="off"
|
|
||||||
className="border-token-border-medium mb-2 h-9 w-full resize-none overflow-y-auto rounded-lg border px-3 py-2 text-sm outline-none focus:ring-2 focus:ring-blue-400 dark:bg-gray-800"
|
|
||||||
{...register('oauth_client_secret', { required: type === AuthTypeEnum.OAuth })}
|
|
||||||
/>
|
|
||||||
<label className="mb-1 block text-sm font-medium">Authorization URL</label>
|
|
||||||
<input
|
|
||||||
className="border-token-border-medium mb-2 h-9 w-full resize-none overflow-y-auto rounded-lg border px-3 py-2 text-sm outline-none focus:ring-2 focus:ring-blue-400 dark:bg-gray-800"
|
|
||||||
{...register('authorization_url', { required: type === AuthTypeEnum.OAuth })}
|
|
||||||
/>
|
|
||||||
<label className="mb-1 block text-sm font-medium">Token URL</label>
|
|
||||||
<input
|
|
||||||
className="border-token-border-medium mb-2 h-9 w-full resize-none overflow-y-auto rounded-lg border px-3 py-2 text-sm outline-none focus:ring-2 focus:ring-blue-400 dark:bg-gray-800"
|
|
||||||
{...register('client_url', { required: type === AuthTypeEnum.OAuth })}
|
|
||||||
/>
|
|
||||||
<label className="mb-1 block text-sm font-medium">Scope</label>
|
|
||||||
<input
|
|
||||||
className="border-token-border-medium mb-2 h-9 w-full resize-none overflow-y-auto rounded-lg border px-3 py-2 text-sm outline-none focus:ring-2 focus:ring-blue-400 dark:bg-gray-800"
|
|
||||||
{...register('scope', { required: type === AuthTypeEnum.OAuth })}
|
|
||||||
/>
|
|
||||||
<label className="mb-1 block text-sm font-medium">Token Exchange Method</label>
|
|
||||||
<RadioGroup.Root
|
|
||||||
defaultValue={AuthorizationTypeEnum.Basic}
|
|
||||||
onValueChange={(value) => setValue('token_exchange_method', value)}
|
|
||||||
value={token_exchange_method}
|
|
||||||
role="radiogroup"
|
|
||||||
aria-required="true"
|
|
||||||
dir="ltr"
|
|
||||||
tabIndex={0}
|
|
||||||
style={{ outline: 'none' }}
|
|
||||||
>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<label htmlFor=":rj1:" className="flex cursor-pointer items-center gap-1">
|
|
||||||
<RadioGroup.Item
|
|
||||||
type="button"
|
|
||||||
role="radio"
|
|
||||||
value={TokenExchangeMethodEnum.DefaultPost}
|
|
||||||
id=":rj1:"
|
|
||||||
className="mr-1 flex h-5 w-5 items-center justify-center rounded-full border border-gray-500 bg-white dark:border-gray-700 dark:bg-gray-700"
|
|
||||||
tabIndex={-1}
|
|
||||||
>
|
|
||||||
<RadioGroup.Indicator className="h-2 w-2 rounded-full bg-gray-950 dark:bg-white"></RadioGroup.Indicator>
|
|
||||||
</RadioGroup.Item>
|
|
||||||
Default (POST request)
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<label htmlFor=":rj3:" className="flex cursor-pointer items-center gap-1">
|
|
||||||
<RadioGroup.Item
|
|
||||||
type="button"
|
|
||||||
role="radio"
|
|
||||||
value={TokenExchangeMethodEnum.BasicAuthHeader}
|
|
||||||
id=":rj3:"
|
|
||||||
className="mr-1 flex h-5 w-5 items-center justify-center rounded-full border border-gray-500 bg-white dark:border-gray-700 dark:bg-gray-700"
|
|
||||||
tabIndex={-1}
|
|
||||||
>
|
|
||||||
<RadioGroup.Indicator className="h-2 w-2 rounded-full bg-gray-950 dark:bg-white"></RadioGroup.Indicator>
|
|
||||||
</RadioGroup.Item>
|
|
||||||
Basic authorization header
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</RadioGroup.Root>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
@ -14,6 +14,7 @@ import type {
|
||||||
} from 'librechat-data-provider';
|
} from 'librechat-data-provider';
|
||||||
import type { ActionAuthForm } from '~/common';
|
import type { ActionAuthForm } from '~/common';
|
||||||
import type { Spec } from './ActionsTable';
|
import type { Spec } from './ActionsTable';
|
||||||
|
import ActionCallback from '~/components/SidePanel/Builder/ActionCallback';
|
||||||
import { ActionsTable, columns } from './ActionsTable';
|
import { ActionsTable, columns } from './ActionsTable';
|
||||||
import { useUpdateAgentAction } from '~/data-provider';
|
import { useUpdateAgentAction } from '~/data-provider';
|
||||||
import { useToastContext } from '~/Providers';
|
import { useToastContext } from '~/Providers';
|
||||||
|
|
@ -248,8 +249,8 @@ export default function ActionsInput({
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{!!data && (
|
{!!data && (
|
||||||
<div>
|
<div className="my-2">
|
||||||
<div className="mb-1.5 flex items-center">
|
<div className="flex items-center">
|
||||||
<label className="text-token-text-primary block font-medium">
|
<label className="text-token-text-primary block font-medium">
|
||||||
{localize('com_assistants_available_actions')}
|
{localize('com_assistants_available_actions')}
|
||||||
</label>
|
</label>
|
||||||
|
|
@ -258,6 +259,7 @@ export default function ActionsInput({
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="relative my-1">
|
<div className="relative my-1">
|
||||||
|
<ActionCallback action_id={action?.action_id} />
|
||||||
<div className="mb-1.5 flex items-center">
|
<div className="mb-1.5 flex items-center">
|
||||||
<label className="text-token-text-primary block font-medium">
|
<label className="text-token-text-primary block font-medium">
|
||||||
{localize('com_ui_privacy_policy_url')}
|
{localize('com_ui_privacy_policy_url')}
|
||||||
|
|
@ -267,7 +269,7 @@ export default function ActionsInput({
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="https://api.example-weather-app.com/privacy"
|
placeholder="https://api.example-weather-app.com/privacy"
|
||||||
className="flex-1 rounded-lg bg-transparent px-3 py-1.5 text-sm outline-none focus:ring-1 focus:ring-border-light"
|
className="flex-1 rounded-lg bg-transparent px-3 py-1.5 text-sm outline-none placeholder:text-text-secondary-alt focus:ring-1 focus:ring-border-light"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { useForm, FormProvider } from 'react-hook-form';
|
import { useForm, FormProvider } from 'react-hook-form';
|
||||||
import {
|
import {
|
||||||
AuthTypeEnum,
|
AuthTypeEnum,
|
||||||
|
|
@ -7,14 +7,14 @@ import {
|
||||||
} from 'librechat-data-provider';
|
} from 'librechat-data-provider';
|
||||||
import { ChevronLeft } from 'lucide-react';
|
import { ChevronLeft } from 'lucide-react';
|
||||||
import type { AgentPanelProps, ActionAuthForm } from '~/common';
|
import type { AgentPanelProps, ActionAuthForm } from '~/common';
|
||||||
import { Dialog, DialogTrigger, OGDialog, OGDialogTrigger, Label } from '~/components/ui';
|
import ActionsAuth from '~/components/SidePanel/Builder/ActionsAuth';
|
||||||
|
import { OGDialog, OGDialogTrigger, Label } from '~/components/ui';
|
||||||
import OGDialogTemplate from '~/components/ui/OGDialogTemplate';
|
import OGDialogTemplate from '~/components/ui/OGDialogTemplate';
|
||||||
import { useDeleteAgentAction } from '~/data-provider';
|
import { useDeleteAgentAction } from '~/data-provider';
|
||||||
import useLocalize from '~/hooks/useLocalize';
|
import useLocalize from '~/hooks/useLocalize';
|
||||||
import { useToastContext } from '~/Providers';
|
import { useToastContext } from '~/Providers';
|
||||||
import { TrashIcon } from '~/components/svg';
|
import { TrashIcon } from '~/components/svg';
|
||||||
import ActionsInput from './ActionsInput';
|
import ActionsInput from './ActionsInput';
|
||||||
import ActionsAuth from './ActionsAuth';
|
|
||||||
import { Panel } from '~/common';
|
import { Panel } from '~/common';
|
||||||
|
|
||||||
export default function ActionsPanel({
|
export default function ActionsPanel({
|
||||||
|
|
@ -26,8 +26,6 @@ export default function ActionsPanel({
|
||||||
}: AgentPanelProps) {
|
}: AgentPanelProps) {
|
||||||
const localize = useLocalize();
|
const localize = useLocalize();
|
||||||
const { showToast } = useToastContext();
|
const { showToast } = useToastContext();
|
||||||
|
|
||||||
const [openAuthDialog, setOpenAuthDialog] = useState(false);
|
|
||||||
const deleteAgentAction = useDeleteAgentAction({
|
const deleteAgentAction = useDeleteAgentAction({
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
showToast({
|
showToast({
|
||||||
|
|
@ -65,7 +63,6 @@ export default function ActionsPanel({
|
||||||
});
|
});
|
||||||
|
|
||||||
const { reset, watch } = methods;
|
const { reset, watch } = methods;
|
||||||
const type = watch('type');
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (action?.metadata.auth) {
|
if (action?.metadata.auth) {
|
||||||
|
|
@ -156,40 +153,7 @@ export default function ActionsPanel({
|
||||||
<a href="https://help.openai.com/en/articles/8554397-creating-a-gpt" target="_blank" rel="noreferrer" className="font-medium">Learn more.</a>
|
<a href="https://help.openai.com/en/articles/8554397-creating-a-gpt" target="_blank" rel="noreferrer" className="font-medium">Learn more.</a>
|
||||||
</div> */}
|
</div> */}
|
||||||
</div>
|
</div>
|
||||||
<Dialog open={openAuthDialog} onOpenChange={setOpenAuthDialog}>
|
<ActionsAuth />
|
||||||
<DialogTrigger asChild>
|
|
||||||
<div className="relative mb-4">
|
|
||||||
<div className="mb-1.5 flex items-center">
|
|
||||||
<label className="text-token-text-primary block font-medium">
|
|
||||||
{localize('com_ui_authentication')}
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div className="border-token-border-medium flex rounded-lg border text-sm hover:cursor-pointer">
|
|
||||||
<div className="h-9 grow px-3 py-2">{type}</div>
|
|
||||||
<div className="bg-token-border-medium w-px"></div>
|
|
||||||
<button type="button" color="neutral" className="flex items-center gap-2 px-3">
|
|
||||||
<svg
|
|
||||||
width="24"
|
|
||||||
height="24"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="none"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
className="icon-sm"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="M11.6439 3C10.9352 3 10.2794 3.37508 9.92002 3.98596L9.49644 4.70605C8.96184 5.61487 7.98938 6.17632 6.93501 6.18489L6.09967 6.19168C5.39096 6.19744 4.73823 6.57783 4.38386 7.19161L4.02776 7.80841C3.67339 8.42219 3.67032 9.17767 4.01969 9.7943L4.43151 10.5212C4.95127 11.4386 4.95127 12.5615 4.43151 13.4788L4.01969 14.2057C3.67032 14.8224 3.67339 15.5778 4.02776 16.1916L4.38386 16.8084C4.73823 17.4222 5.39096 17.8026 6.09966 17.8083L6.93502 17.8151C7.98939 17.8237 8.96185 18.3851 9.49645 19.294L9.92002 20.014C10.2794 20.6249 10.9352 21 11.6439 21H12.3561C13.0648 21 13.7206 20.6249 14.08 20.014L14.5035 19.294C15.0381 18.3851 16.0106 17.8237 17.065 17.8151L17.9004 17.8083C18.6091 17.8026 19.2618 17.4222 19.6162 16.8084L19.9723 16.1916C20.3267 15.5778 20.3298 14.8224 19.9804 14.2057L19.5686 13.4788C19.0488 12.5615 19.0488 11.4386 19.5686 10.5212L19.9804 9.7943C20.3298 9.17767 20.3267 8.42219 19.9723 7.80841L19.6162 7.19161C19.2618 6.57783 18.6091 6.19744 17.9004 6.19168L17.065 6.18489C16.0106 6.17632 15.0382 5.61487 14.5036 4.70605L14.08 3.98596C13.7206 3.37508 13.0648 3 12.3561 3H11.6439Z"
|
|
||||||
stroke="currentColor"
|
|
||||||
strokeWidth="2"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
/>
|
|
||||||
<circle cx="12" cy="12" r="2.5" stroke="currentColor" strokeWidth="2" />
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</DialogTrigger>
|
|
||||||
<ActionsAuth setOpenAuthDialog={setOpenAuthDialog} />
|
|
||||||
</Dialog>
|
|
||||||
<ActionsInput action={action} agent_id={agent_id} setAction={setAction} />
|
<ActionsInput action={action} agent_id={agent_id} setAction={setAction} />
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue