Compare commits

...

68 Commits

Author SHA1 Message Date
Marco Beretta
014060a11b chore: update @radix-ui/react-accordion to version 1.2.11 2025-07-26 15:03:26 +02:00
Marco Beretta
80d406b629 fix: merge conflicts 2025-07-26 03:07:03 +02:00
Marco Beretta
22035bbf95 style: update itemClassName in ToolsDropdown for improved UI consistency 2025-07-26 01:55:17 +02:00
Marco Beretta
17a164b420 refactor: enhance Menu component to support custom render functions for menu items 2025-07-26 01:55:16 +02:00
Marco Beretta
5b663d0e35 chore: remove unused imports and clean up code in NewChat component 2025-07-26 01:55:16 +02:00
Marco Beretta
c273dfc1f4 refactor: code structure; chore: translations cleanup 2025-07-26 01:55:16 +02:00
Marco Beretta
de69bcdd64 chore: update package version to 0.1.5 and adjust peer dependencies
- Bump version in package.json from 0.1.4 to 0.1.5.
- Update peer dependency for @tanstack/react-query to allow version 5.0.0.
- Add @tanstack/react-table and @tanstack/react-virtual as dependencies.
- Update various dependencies to their latest compatible versions.
- Simplify postcss.config.js by removing unnecessary options.
- Clean up rollup.config.js by removing ignored PostCSS warnings.
- Update CheckboxButton component to cast icon as React JSX element.
- Adjust Combobox component's class names for better styling.
- Change DropdownPopup component to use React's namespace import.
- Modify InputOTP component to use 'any' type for OTPInputContext.
- Ensure displayLabel and value in ModelParameters are converted to strings.
- Update MultiSearch component's placeholder to ensure it's a string.
- Cast selectIcon in MultiSelect as React JSX element for consistency.
- Update OGDialogTemplate to cast selectText as React JSX element.
- Initialize animationRef in PixelCard with undefined for clarity.
- Add TypeScript ignore comments in Select and SelectDropDown components for Radix UI type conflicts.
- Ensure title in SelectDropDown is a string and adjust rendering of options.
- Update useLocalize hook to cast options as any for compatibility.
2025-07-26 01:55:14 +02:00
Marco Beretta
59412c2b36 chore: bump client version to 0.1.4 and update @react-spring dependencies 2025-07-26 01:48:11 +02:00
Marco Beretta
0af8fba7ca chore: bump client version to 0.1.3 and update dependencies 2025-07-26 01:48:11 +02:00
Marco Beretta
d5cf83313b chore: bump client version to 0.1.2 in package-lock.json 2025-07-26 01:48:11 +02:00
Marco Beretta
355da0fc2e chore: bump version to 0.1.2 in package.json 2025-07-26 01:48:11 +02:00
Marco Beretta
8ba5aa6055 refactor: update package dependencies and improve PostCSS and Rollup configurations 2025-07-26 01:48:11 +02:00
Marco Beretta
f1204531a8 refactor: clean up client workflow and update package dependencies 2025-07-26 01:48:11 +02:00
Marco Beretta
a65e33758d refactor: update peer dependencies and improve component typings
- Removed duplicate peer dependencies from package.json and organized them.
- Updated rollup.config.js to disable TypeScript checking during the build process.
- Modified AnimatedTabs component to use React.ReactNode for label and content types, and added TypeScript workarounds for compatibility.
- Enhanced Label and Separator components to accept an optional className prop and improved prop spreading.
- Updated Slider component to include an optional className prop and refined prop handling for better type safety.
2025-07-26 01:48:11 +02:00
Marco Beretta
0855631c54 refactor: remove unused types and interfaces from common files 2025-07-26 01:48:11 +02:00
Marco Beretta
a1e052871f refactor: reorganize exports in index.ts for improved clarity 2025-07-26 01:48:11 +02:00
Marco Beretta
ca18ada9e2 feat: update package.json and rollup.config.js to include jotai and enhance bundling configuration 2025-07-26 01:48:11 +02:00
Marco Beretta
103af99879 feat: update package.json and rollup.config.js to include jotai and enhance bundling configuration 2025-07-26 01:48:11 +02:00
Marco Beretta
412948e025 chore: move dependencies to peerDependencies in package.json 2025-07-26 01:48:11 +02:00
Marco Beretta
4da25826d9 chore: move @tanstack/react-virtual to dependencies and remove recoil from package.json 2025-07-26 01:48:11 +02:00
Marco Beretta
6e7fdeb3a3 feat: integrate Tailwind CSS and update theme context for improved styling 2025-07-26 01:48:11 +02:00
Marco Beretta
1c3f5b972d chore: remove unnecessary logging of sttExternal and ttsExternal in Speech component 2025-07-26 01:48:10 +02:00
Danny Avila
e7aa83e073 chore: update tsconfig.json to exclude additional test files 2025-07-26 01:48:10 +02:00
Danny Avila
0b5155d277 chore: add missing @testing-library/react dependency to client package 2025-07-26 01:48:10 +02:00
Danny Avila
86deb4d19a chore: update GitHub Actions workflow to install and build data-provider and client dependencies 2025-07-26 01:48:10 +02:00
Danny Avila
f741a59ec4 feat: update GitHub Actions workflow to publish @librechat/client 2025-07-26 01:48:10 +02:00
Danny Avila
83477bba34 chore: linting 2025-07-26 01:48:10 +02:00
Danny Avila
5476029bca refactor: remove unused defaultSelectedValues prop from MCPSelect and MultiSelect components 2025-07-26 01:48:09 +02:00
Danny Avila
05a0a1f7cd chore: linting 2025-07-26 01:47:35 +02:00
Danny Avila
55f67212d5 feat: add react-hook-form dependency and update FormInput component to use its types 2025-07-26 01:47:35 +02:00
Danny Avila
63d0c301a0 test: update AgentFooter tests to use document.querySelector for spinner checks
test: mock window.matchMedia in setupTests.js for consistent test environment
2025-07-26 01:47:35 +02:00
Danny Avila
bac6e499b7 chore: include "packages/client" in unused i18next keys detection 2025-07-26 01:47:35 +02:00
Danny Avila
59de92afa9 fix: add missing peer dependency for @radix-ui/react-collapsible 2025-07-26 01:47:35 +02:00
Danny Avila
e297386cee chore: enhance unused package detection for @librechat/client and improve dependency extraction 2025-07-26 01:47:34 +02:00
Danny Avila
e2b1cc607f fix: correct frontend:ci script to include client package build 2025-07-26 01:47:19 +02:00
Danny Avila
55bda03d19 fix: add remaining peer dependencies and match actual versions previously used in client/package.json 2025-07-26 01:47:19 +02:00
Danny Avila
b11ba35790 fix: circular dependencies in DataTable 2025-07-26 01:47:18 +02:00
Danny Avila
8e1b00da2a fix: correct useSprings implementation in SplitText component 2025-07-26 01:47:18 +02:00
Danny Avila
39f5dd47dc fix: update peer dependencies in @librechat/client to prevent bundling them 2025-07-26 01:47:18 +02:00
Marco Beretta
16f83c6e8e chore: fixed formatting issues 2025-07-26 01:47:18 +02:00
Marco Beretta
b71a82d0e9 feat: /client transition to @librechat/client 2025-07-26 01:47:17 +02:00
Danny Avila
63a5902404 feat: export Toast component in client package
- Added export for the Toast component in index.ts to enhance modularity and accessibility of components.
2025-07-26 01:14:16 +02:00
Danny Avila
21c3a831c3 feat: add client package directory to update configuration
- Included the 'client' package directory in the update.js configuration to ensure it is recognized during updates.
2025-07-26 01:14:16 +02:00
Danny Avila
ae43b4eed0 feat: update client package dependencies and Rollup configuration
- Added rollup-plugin-postcss to package.json and updated package-lock.json.
- Enhanced Rollup configuration to include postcss plugin for CSS handling.
- Updated index.ts to export all components from the components directory for better modularity.
2025-07-26 01:14:14 +02:00
Danny Avila
2af4ca5b5c chore: rename package/client build script command 2025-07-26 01:13:50 +02:00
Danny Avila
b6c7b0bc71 feat: enhance Rollup configuration for client package
- Updated terser plugin settings to preserve directives like 'use client'.
- Added custom warning handler to ignore "use client" directive warnings during the build process.
2025-07-26 01:13:50 +02:00
Danny Avila
f8738b207c feat: update client package configuration and dependencies
- Added new dependencies for Rollup plugins and updated existing ones in package.json and package-lock.json.
- Introduced a new Rollup configuration file for building the client package.
- Refactored build scripts to include a dedicated build command for the client.
- Updated TypeScript configuration for improved module resolution and type declaration output.
- Integrated a Toast component from the client package into the main App component.
2025-07-26 01:13:50 +02:00
Danny Avila
6ea1d5eab2 feat: add @librechat/client as a dependency in package.json and package-lock.json 2025-07-26 01:13:50 +02:00
Danny Avila
992911514c fix: move dependencies to peerDependencies in client package 2025-07-26 01:13:50 +02:00
Marco Beretta
9289aeb2ba feat: add back recoil 2025-07-26 01:13:50 +02:00
Marco Beretta
898d273aaf fix: package; refactor: tsconfig 2025-07-26 01:13:50 +02:00
Marco Beretta
5859350bcb fix: cleanup 2025-07-26 01:13:50 +02:00
Marco Beretta
882cca247a feat: cleanup unused types from common/index.ts
- Remove 104 unused type exports from packages/client/src/common/index.ts
- Keep only 7 actually used exports (93% reduction)
- Add cleanup script with enhanced import pattern detection
- Support both named imports and namespace imports (* as t)
- Create automatic backups and comprehensive documentation
- Maintain type safety with build verification
- No breaking changes to existing code

Kept exports:
- TShowToast, Option, OptionWithIcon, DropdownValueSetter
- MentionOption, NotificationSeverity, MenuItemProps

Scripts: cleanup-common-types-safe.js, README-CLEANUP.md
2025-07-26 01:13:50 +02:00
Marco Beretta
0b7dd55797 fix build client package 2025-07-26 01:13:50 +02:00
Marco Beretta
9f270127d3 feat: Add jotai as a peer dependency 2025-07-26 01:13:50 +02:00
Marco Beretta
1380db85cb feat: Add common types and interfaces for accessibility, agents, artifacts, assistants, and tools 2025-07-26 01:13:50 +02:00
Marco Beretta
dcaa5af598 feat: init @librechat/client 2025-07-26 01:13:49 +02:00
Dustin Healy
545a909953 🗂️ refactor: Make MCPSubMenu consistent with MCPSelect (#8650)
- Refactored MCPSelect and MCPSubMenu components to utilize a new custom hook, `useMCPServerManager`, for improved state management and server initialization logic.
- Added functionality to handle simultaneous MCP server initialization requests, including cancellation and user notifications.
- Updated translation files to include new messages for initialization cancellation.
- Improved the configuration dialog handling for MCP servers, streamlining the user experience when managing server settings.
2025-07-25 14:51:42 -04:00
Danny Avila
cd436dc6a8 📦 chore: Update @modelcontextprotocol/sdk to v1.17.0 (#8674)
* 📦 chore: Update `@modelcontextprotocol/sdk` to v1.17.0

* refactor: unused package detection by extracting workspace dependencies in GitHub Actions workflow

* chore: Enhance unused package detection by including peerDependencies extraction in GitHub Actions workflow

* fix: Ensure safe extraction of dependencies and peerDependencies in unused package detection workflow
2025-07-25 14:06:16 -04:00
Danny Avila
e75beb92b3 🗑️ chore: Remove Workflows for Changelogs (#8673) 2025-07-25 13:45:22 -04:00
Danny Avila
5251246313 📱 refactor: Redis Client Error Logging and Ping only when Ready (#8671)
* 📱 refactor: Redis Client Error Logging and Ping only when Ready

* chore: intellisense for warning comment for Keyv Redis client regarding prefix support
2025-07-25 12:33:05 -04:00
Danny Avila
26f23c6aaf 📦 chore: Bump @node-saml/passport-saml to v5.1.0 (#8670) 2025-07-25 11:26:20 -04:00
Danny Avila
1636af1f27 📦 chore: Bump mongodb-memory-server to v10.1.4 (#8669) 2025-07-25 11:23:38 -04:00
Theo N. Truong
b050a0bf1e feat: Add Redis Ping Interval Configuration (#8648)
Co-authored-by: Danny Avila <danny@librechat.ai>
2025-07-25 11:00:02 -04:00
github-actions[bot]
deb928bf80 🌍 i18n: Update translation.json with latest translations (#8664)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-07-25 10:36:14 -04:00
Theo N. Truong
21005b66cc feat: Add support for forced in-memory cache namespaces configuration (#8586)
*  feat: Add support for forced in-memory cache keys configuration

* refactor: Update cache keys to use uppercase constants and moved cache for `librechat.yaml` into its own cache namespace (STATIC_CONFIG) and with a more descriptive key (LIBRECHAT_YAML_CONFIG)
2025-07-25 10:32:55 -04:00
Dustin Healy
3dc9e85fab 🐛 fix: Display OAuth MCP servers according to Chat Menu Setting (#8643)
* fix: chatMenu not being respected in MCPSelect

* fix: chatMenu not being respected in MCPSubMenu
2025-07-25 10:21:10 -04:00
Sebastien Bruel
ec67cf2d3a 🚇 chore: Remove Overridden Transport Error Listener (#8656) 2025-07-25 10:17:33 -04:00
577 changed files with 7284 additions and 2798 deletions

View File

@@ -627,6 +627,15 @@ HELP_AND_FAQ_URL=https://librechat.ai
# Redis connection limits
# REDIS_MAX_LISTENERS=40
# Redis ping interval in seconds (0 = disabled, >0 = enabled)
# When set to a positive integer, Redis clients will ping the server at this interval to keep connections alive
# When unset or 0, no pinging is performed (recommended for most use cases)
# REDIS_PING_INTERVAL=300
# Force specific cache namespaces to use in-memory storage even when Redis is enabled
# Comma-separated list of CacheKeys (e.g., STATIC_CONFIG,ROLES,MESSAGES)
# FORCED_IN_MEMORY_CACHE_NAMESPACES=STATIC_CONFIG,ROLES
#==================================================#
# Others #
#==================================================#

View File

@@ -1,6 +1,11 @@
name: Publish `@librechat/client` to NPM
on:
push:
branches:
- main
paths:
- 'packages/client/package.json'
workflow_dispatch:
inputs:
reason:
@@ -17,16 +22,37 @@ jobs:
- name: Use Node.js
uses: actions/setup-node@v4
with:
node-version: '18.x'
- name: Check if client package exists
node-version: '20.x'
- name: Install client dependencies
run: cd packages/client && npm ci
- name: Build client
run: cd packages/client && npm run build
- name: Set up npm authentication
run: echo "//registry.npmjs.org/:_authToken=${{ secrets.PUBLISH_NPM_TOKEN }}" > ~/.npmrc
- name: Check version change
id: check
working-directory: packages/client
run: |
if [ -d "packages/client" ]; then
echo "Client package directory found"
PACKAGE_VERSION=$(node -p "require('./package.json').version")
PUBLISHED_VERSION=$(npm view @librechat/client version 2>/dev/null || echo "0.0.0")
if [ "$PACKAGE_VERSION" = "$PUBLISHED_VERSION" ]; then
echo "No version change, skipping publish"
echo "skip=true" >> $GITHUB_OUTPUT
else
echo "Client package directory not found - workflow ready for future use"
exit 0
echo "Version changed, proceeding with publish"
echo "skip=false" >> $GITHUB_OUTPUT
fi
- name: Placeholder for future publishing
run: echo "Client package publishing workflow is ready"
- name: Pack package
if: steps.check.outputs.skip != 'true'
working-directory: packages/client
run: npm pack
- name: Publish
if: steps.check.outputs.skip != 'true'
working-directory: packages/client
run: npm publish *.tgz --access public

View File

@@ -1,95 +0,0 @@
name: Generate Release Changelog PR
on:
push:
tags:
- 'v*.*.*'
workflow_dispatch:
jobs:
generate-release-changelog-pr:
permissions:
contents: write # Needed for pushing commits and creating branches.
pull-requests: write
runs-on: ubuntu-latest
steps:
# 1. Checkout the repository (with full history).
- name: Checkout Repository
uses: actions/checkout@v4
with:
fetch-depth: 0
# 2. Generate the release changelog using our custom configuration.
- name: Generate Release Changelog
id: generate_release
uses: mikepenz/release-changelog-builder-action@v5.1.0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
configuration: ".github/configuration-release.json"
owner: ${{ github.repository_owner }}
repo: ${{ github.event.repository.name }}
outputFile: CHANGELOG-release.md
# 3. Update the main CHANGELOG.md:
# - If it doesn't exist, create it with a basic header.
# - Remove the "Unreleased" section (if present).
# - Prepend the new release changelog above previous releases.
# - Remove all temporary files before committing.
- name: Update CHANGELOG.md
run: |
# Determine the release tag, e.g. "v1.2.3"
TAG=${GITHUB_REF##*/}
echo "Using release tag: $TAG"
# Ensure CHANGELOG.md exists; if not, create a basic header.
if [ ! -f CHANGELOG.md ]; then
echo "# Changelog" > CHANGELOG.md
echo "" >> CHANGELOG.md
echo "All notable changes to this project will be documented in this file." >> CHANGELOG.md
echo "" >> CHANGELOG.md
fi
echo "Updating CHANGELOG.md…"
# Remove the "Unreleased" section (from "## [Unreleased]" until the first occurrence of '---') if it exists.
if grep -q "^## \[Unreleased\]" CHANGELOG.md; then
awk '/^## \[Unreleased\]/{flag=1} flag && /^---/{flag=0; next} !flag' CHANGELOG.md > CHANGELOG.cleaned
else
cp CHANGELOG.md CHANGELOG.cleaned
fi
# Split the cleaned file into:
# - header.md: content before the first release header ("## [v...").
# - tail.md: content from the first release header onward.
awk '/^## \[v/{exit} {print}' CHANGELOG.cleaned > header.md
awk 'f{print} /^## \[v/{f=1; print}' CHANGELOG.cleaned > tail.md
# Combine header, the new release changelog, and the tail.
echo "Combining updated changelog parts..."
cat header.md CHANGELOG-release.md > CHANGELOG.md.new
echo "" >> CHANGELOG.md.new
cat tail.md >> CHANGELOG.md.new
mv CHANGELOG.md.new CHANGELOG.md
# Remove temporary files.
rm -f CHANGELOG.cleaned header.md tail.md CHANGELOG-release.md
echo "Final CHANGELOG.md content:"
cat CHANGELOG.md
# 4. Create (or update) the Pull Request with the updated CHANGELOG.md.
- name: Create Pull Request
uses: peter-evans/create-pull-request@v7
with:
token: ${{ secrets.GITHUB_TOKEN }}
sign-commits: true
commit-message: "chore: update CHANGELOG for release ${{ github.ref_name }}"
base: main
branch: "changelog/${{ github.ref_name }}"
reviewers: danny-avila
title: "📜 docs: Changelog for release ${{ github.ref_name }}"
body: |
**Description**:
- This PR updates the CHANGELOG.md by removing the "Unreleased" section and adding new release notes for release ${{ github.ref_name }} above previous releases.

View File

@@ -1,107 +0,0 @@
name: Generate Unreleased Changelog PR
on:
schedule:
- cron: "0 0 * * 1" # Runs every Monday at 00:00 UTC
workflow_dispatch:
jobs:
generate-unreleased-changelog-pr:
permissions:
contents: write # Needed for pushing commits and creating branches.
pull-requests: write
runs-on: ubuntu-latest
steps:
# 1. Checkout the repository on main.
- name: Checkout Repository on Main
uses: actions/checkout@v4
with:
ref: main
fetch-depth: 0
# 4. Get the latest version tag.
- name: Get Latest Tag
id: get_latest_tag
run: |
LATEST_TAG=$(git describe --tags $(git rev-list --tags --max-count=1) || echo "none")
echo "Latest tag: $LATEST_TAG"
echo "tag=$LATEST_TAG" >> $GITHUB_OUTPUT
# 5. Generate the Unreleased changelog.
- name: Generate Unreleased Changelog
id: generate_unreleased
uses: mikepenz/release-changelog-builder-action@v5.1.0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
configuration: ".github/configuration-unreleased.json"
owner: ${{ github.repository_owner }}
repo: ${{ github.event.repository.name }}
outputFile: CHANGELOG-unreleased.md
fromTag: ${{ steps.get_latest_tag.outputs.tag }}
toTag: main
# 7. Update CHANGELOG.md with the new Unreleased section.
- name: Update CHANGELOG.md
id: update_changelog
run: |
# Create CHANGELOG.md if it doesn't exist.
if [ ! -f CHANGELOG.md ]; then
echo "# Changelog" > CHANGELOG.md
echo "" >> CHANGELOG.md
echo "All notable changes to this project will be documented in this file." >> CHANGELOG.md
echo "" >> CHANGELOG.md
fi
echo "Updating CHANGELOG.md…"
# Extract content before the "## [Unreleased]" (or first version header if missing).
if grep -q "^## \[Unreleased\]" CHANGELOG.md; then
awk '/^## \[Unreleased\]/{exit} {print}' CHANGELOG.md > CHANGELOG_TMP.md
else
awk '/^## \[v/{exit} {print}' CHANGELOG.md > CHANGELOG_TMP.md
fi
# Append the generated Unreleased changelog.
echo "" >> CHANGELOG_TMP.md
cat CHANGELOG-unreleased.md >> CHANGELOG_TMP.md
echo "" >> CHANGELOG_TMP.md
# Append the remainder of the original changelog (starting from the first version header).
awk 'f{print} /^## \[v/{f=1; print}' CHANGELOG.md >> CHANGELOG_TMP.md
# Replace the old file with the updated file.
mv CHANGELOG_TMP.md CHANGELOG.md
# Remove the temporary generated file.
rm -f CHANGELOG-unreleased.md
echo "Final CHANGELOG.md:"
cat CHANGELOG.md
# 8. Check if CHANGELOG.md has any updates.
- name: Check for CHANGELOG.md changes
id: changelog_changes
run: |
if git diff --quiet CHANGELOG.md; then
echo "has_changes=false" >> $GITHUB_OUTPUT
else
echo "has_changes=true" >> $GITHUB_OUTPUT
fi
# 9. Create (or update) the Pull Request only if there are changes.
- name: Create Pull Request
if: steps.changelog_changes.outputs.has_changes == 'true'
uses: peter-evans/create-pull-request@v7
with:
token: ${{ secrets.GITHUB_TOKEN }}
base: main
branch: "changelog/unreleased-update"
sign-commits: true
commit-message: "action: update Unreleased changelog"
title: "📜 docs: Unreleased Changelog"
body: |
**Description**:
- This PR updates the Unreleased section in CHANGELOG.md.
- It compares the current main branch with the latest version tag (determined as ${{ steps.get_latest_tag.outputs.tag }}),
regenerates the Unreleased changelog, removes any old Unreleased block, and inserts the new content.

View File

@@ -6,6 +6,7 @@ on:
- "client/src/**"
- "api/**"
- "packages/data-provider/src/**"
- "packages/client/**"
jobs:
detect-unused-i18n-keys:
@@ -23,7 +24,7 @@ jobs:
# Define paths
I18N_FILE="client/src/locales/en/translation.json"
SOURCE_DIRS=("client/src" "api" "packages/data-provider/src")
SOURCE_DIRS=("client/src" "api" "packages/data-provider/src" "packages/client")
# Check if translation file exists
if [[ ! -f "$I18N_FILE" ]]; then

View File

@@ -7,6 +7,7 @@ on:
- 'package-lock.json'
- 'client/**'
- 'api/**'
- 'packages/client/**'
jobs:
detect-unused-packages:
@@ -28,7 +29,7 @@ jobs:
- name: Validate JSON files
run: |
for FILE in package.json client/package.json api/package.json; do
for FILE in package.json client/package.json api/package.json packages/client/package.json; do
if [[ -f "$FILE" ]]; then
jq empty "$FILE" || (echo "::error title=Invalid JSON::$FILE is invalid" && exit 1)
fi
@@ -63,12 +64,31 @@ jobs:
local folder=$1
local output_file=$2
if [[ -d "$folder" ]]; then
grep -rEho "require\\(['\"]([a-zA-Z0-9@/._-]+)['\"]\\)" "$folder" --include=\*.{js,ts,mjs,cjs} | \
# Extract require() statements
grep -rEho "require\\(['\"]([a-zA-Z0-9@/._-]+)['\"]\\)" "$folder" --include=\*.{js,ts,tsx,jsx,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} | \
# Extract ES6 imports - various patterns
# import x from 'module'
grep -rEho "import .* from ['\"]([a-zA-Z0-9@/._-]+)['\"]" "$folder" --include=\*.{js,ts,tsx,jsx,mjs,cjs} | \
sed -E "s/import .* from ['\"]([a-zA-Z0-9@/._-]+)['\"]/\1/" >> "$output_file"
# import 'module' (side-effect imports)
grep -rEho "import ['\"]([a-zA-Z0-9@/._-]+)['\"]" "$folder" --include=\*.{js,ts,tsx,jsx,mjs,cjs} | \
sed -E "s/import ['\"]([a-zA-Z0-9@/._-]+)['\"]/\1/" >> "$output_file"
# export { x } from 'module' or export * from 'module'
grep -rEho "export .* from ['\"]([a-zA-Z0-9@/._-]+)['\"]" "$folder" --include=\*.{js,ts,tsx,jsx,mjs,cjs} | \
sed -E "s/export .* from ['\"]([a-zA-Z0-9@/._-]+)['\"]/\1/" >> "$output_file"
# import type { x } from 'module' (TypeScript)
grep -rEho "import type .* from ['\"]([a-zA-Z0-9@/._-]+)['\"]" "$folder" --include=\*.{ts,tsx} | \
sed -E "s/import type .* from ['\"]([a-zA-Z0-9@/._-]+)['\"]/\1/" >> "$output_file"
# Remove subpath imports but keep the base package
# e.g., '@tanstack/react-query/devtools' becomes '@tanstack/react-query'
sed -i -E 's|^(@?[a-zA-Z0-9-]+(/[a-zA-Z0-9-]+)?)/.*|\1|' "$output_file"
sort -u "$output_file" -o "$output_file"
else
touch "$output_file"
@@ -78,13 +98,80 @@ jobs:
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
# Extract dependencies used by @librechat/client package
extract_deps_from_code "packages/client" packages_client_used_code.txt
- name: Get @librechat/client dependencies
id: get-librechat-client-deps
run: |
if [[ -f "packages/client/package.json" ]]; then
# Get all dependencies from @librechat/client (dependencies, devDependencies, and peerDependencies)
DEPS=$(jq -r '.dependencies // {} | keys[]' packages/client/package.json 2>/dev/null || echo "")
DEV_DEPS=$(jq -r '.devDependencies // {} | keys[]' packages/client/package.json 2>/dev/null || echo "")
PEER_DEPS=$(jq -r '.peerDependencies // {} | keys[]' packages/client/package.json 2>/dev/null || echo "")
# Combine all dependencies
echo "$DEPS" > librechat_client_deps.txt
echo "$DEV_DEPS" >> librechat_client_deps.txt
echo "$PEER_DEPS" >> librechat_client_deps.txt
# Also include dependencies that are imported in packages/client
cat packages_client_used_code.txt >> librechat_client_deps.txt
# Remove empty lines and sort
grep -v '^$' librechat_client_deps.txt | sort -u > temp_deps.txt
mv temp_deps.txt librechat_client_deps.txt
else
touch librechat_client_deps.txt
fi
- name: Extract Workspace Dependencies
id: extract-workspace-deps
run: |
# Function to get dependencies from a workspace package that are used by another package
get_workspace_package_deps() {
local package_json=$1
local output_file=$2
# Get all workspace dependencies (starting with @librechat/)
if [[ -f "$package_json" ]]; then
local workspace_deps=$(jq -r '.dependencies // {} | to_entries[] | select(.key | startswith("@librechat/")) | .key' "$package_json" 2>/dev/null || echo "")
# For each workspace dependency, get its dependencies
for dep in $workspace_deps; do
# Convert @librechat/api to packages/api
local workspace_path=$(echo "$dep" | sed 's/@librechat\//packages\//')
local workspace_package_json="${workspace_path}/package.json"
if [[ -f "$workspace_package_json" ]]; then
# Extract all dependencies from the workspace package
jq -r '.dependencies // {} | keys[]' "$workspace_package_json" 2>/dev/null >> "$output_file"
# Also extract peerDependencies
jq -r '.peerDependencies // {} | keys[]' "$workspace_package_json" 2>/dev/null >> "$output_file"
fi
done
fi
if [[ -f "$output_file" ]]; then
sort -u "$output_file" -o "$output_file"
else
touch "$output_file"
fi
}
# Get workspace dependencies for each package
get_workspace_package_deps "package.json" root_workspace_deps.txt
get_workspace_package_deps "client/package.json" client_workspace_deps.txt
get_workspace_package_deps "api/package.json" api_workspace_deps.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 "")
# Exclude dependencies used in scripts, code, and workspace packages
UNUSED=$(comm -23 <(echo "$UNUSED" | sort) <(cat root_used_deps.txt root_used_code.txt root_workspace_deps.txt | sort) || echo "")
echo "ROOT_UNUSED<<EOF" >> $GITHUB_ENV
echo "$UNUSED" >> $GITHUB_ENV
echo "EOF" >> $GITHUB_ENV
@@ -97,7 +184,8 @@ jobs:
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 "")
# Exclude dependencies used in scripts, code, and workspace packages
UNUSED=$(comm -23 <(echo "$UNUSED" | sort) <(cat ../client_used_deps.txt ../client_used_code.txt ../client_workspace_deps.txt | sort) || echo "")
# Filter out false positives
UNUSED=$(echo "$UNUSED" | grep -v "^micromark-extension-llm-math$" || echo "")
echo "CLIENT_UNUSED<<EOF" >> $GITHUB_ENV
@@ -113,7 +201,8 @@ jobs:
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 "")
# Exclude dependencies used in scripts, code, and workspace packages
UNUSED=$(comm -23 <(echo "$UNUSED" | sort) <(cat ../api_used_deps.txt ../api_used_code.txt ../api_workspace_deps.txt | sort) || echo "")
echo "API_UNUSED<<EOF" >> $GITHUB_ENV
echo "$UNUSED" >> $GITHUB_ENV
echo "EOF" >> $GITHUB_ENV

View File

@@ -1,5 +1,6 @@
const fs = require('fs');
const { math, isEnabled } = require('@librechat/api');
const { CacheKeys } = require('librechat-data-provider');
// To ensure that different deployments do not interfere with each other's cache, we use a prefix for the Redis keys.
// This prefix is usually the deployment ID, which is often passed to the container or pod as an env var.
@@ -15,7 +16,26 @@ if (USE_REDIS && !process.env.REDIS_URI) {
throw new Error('USE_REDIS is enabled but REDIS_URI is not set.');
}
// Comma-separated list of cache namespaces that should be forced to use in-memory storage
// even when Redis is enabled. This allows selective performance optimization for specific caches.
const FORCED_IN_MEMORY_CACHE_NAMESPACES = process.env.FORCED_IN_MEMORY_CACHE_NAMESPACES
? process.env.FORCED_IN_MEMORY_CACHE_NAMESPACES.split(',').map((key) => key.trim())
: [];
// Validate against CacheKeys enum
if (FORCED_IN_MEMORY_CACHE_NAMESPACES.length > 0) {
const validKeys = Object.values(CacheKeys);
const invalidKeys = FORCED_IN_MEMORY_CACHE_NAMESPACES.filter((key) => !validKeys.includes(key));
if (invalidKeys.length > 0) {
throw new Error(
`Invalid cache keys in FORCED_IN_MEMORY_CACHE_NAMESPACES: ${invalidKeys.join(', ')}. Valid keys: ${validKeys.join(', ')}`,
);
}
}
const cacheConfig = {
FORCED_IN_MEMORY_CACHE_NAMESPACES,
USE_REDIS,
REDIS_URI: process.env.REDIS_URI,
REDIS_USERNAME: process.env.REDIS_USERNAME,
@@ -23,6 +43,7 @@ const cacheConfig = {
REDIS_CA: process.env.REDIS_CA ? fs.readFileSync(process.env.REDIS_CA, 'utf8') : null,
REDIS_KEY_PREFIX: process.env[REDIS_KEY_PREFIX_VAR] || REDIS_KEY_PREFIX || '',
REDIS_MAX_LISTENERS: math(process.env.REDIS_MAX_LISTENERS, 40),
REDIS_PING_INTERVAL: math(process.env.REDIS_PING_INTERVAL, 0),
CI: isEnabled(process.env.CI),
DEBUG_MEMORY_CACHE: isEnabled(process.env.DEBUG_MEMORY_CACHE),

View File

@@ -14,6 +14,8 @@ describe('cacheConfig', () => {
delete process.env.REDIS_KEY_PREFIX_VAR;
delete process.env.REDIS_KEY_PREFIX;
delete process.env.USE_REDIS;
delete process.env.REDIS_PING_INTERVAL;
delete process.env.FORCED_IN_MEMORY_CACHE_NAMESPACES;
// Clear require cache
jest.resetModules();
@@ -105,4 +107,51 @@ describe('cacheConfig', () => {
expect(cacheConfig.REDIS_CA).toBeNull();
});
});
describe('REDIS_PING_INTERVAL configuration', () => {
test('should default to 0 when REDIS_PING_INTERVAL is not set', () => {
const { cacheConfig } = require('./cacheConfig');
expect(cacheConfig.REDIS_PING_INTERVAL).toBe(0);
});
test('should use provided REDIS_PING_INTERVAL value', () => {
process.env.REDIS_PING_INTERVAL = '300';
const { cacheConfig } = require('./cacheConfig');
expect(cacheConfig.REDIS_PING_INTERVAL).toBe(300);
});
});
describe('FORCED_IN_MEMORY_CACHE_NAMESPACES validation', () => {
test('should parse comma-separated cache keys correctly', () => {
process.env.FORCED_IN_MEMORY_CACHE_NAMESPACES = ' ROLES, STATIC_CONFIG ,MESSAGES ';
const { cacheConfig } = require('./cacheConfig');
expect(cacheConfig.FORCED_IN_MEMORY_CACHE_NAMESPACES).toEqual([
'ROLES',
'STATIC_CONFIG',
'MESSAGES',
]);
});
test('should throw error for invalid cache keys', () => {
process.env.FORCED_IN_MEMORY_CACHE_NAMESPACES = 'INVALID_KEY,ROLES';
expect(() => {
require('./cacheConfig');
}).toThrow('Invalid cache keys in FORCED_IN_MEMORY_CACHE_NAMESPACES: INVALID_KEY');
});
test('should handle empty string gracefully', () => {
process.env.FORCED_IN_MEMORY_CACHE_NAMESPACES = '';
const { cacheConfig } = require('./cacheConfig');
expect(cacheConfig.FORCED_IN_MEMORY_CACHE_NAMESPACES).toEqual([]);
});
test('should handle undefined env var gracefully', () => {
const { cacheConfig } = require('./cacheConfig');
expect(cacheConfig.FORCED_IN_MEMORY_CACHE_NAMESPACES).toEqual([]);
});
});
});

View File

@@ -16,7 +16,10 @@ const { RedisStore } = require('rate-limit-redis');
* @returns {Keyv} Cache instance.
*/
const standardCache = (namespace, ttl = undefined, fallbackStore = undefined) => {
if (cacheConfig.USE_REDIS) {
if (
cacheConfig.USE_REDIS &&
!cacheConfig.FORCED_IN_MEMORY_CACHE_NAMESPACES?.includes(namespace)
) {
const keyvRedis = new KeyvRedis(keyvRedisClient);
const cache = new Keyv(keyvRedis, { namespace, ttl });
keyvRedis.namespace = cacheConfig.REDIS_KEY_PREFIX;

View File

@@ -31,6 +31,7 @@ jest.mock('./cacheConfig', () => ({
cacheConfig: {
USE_REDIS: false,
REDIS_KEY_PREFIX: 'test',
FORCED_IN_MEMORY_CACHE_NAMESPACES: [],
},
}));
@@ -63,6 +64,7 @@ describe('cacheFactory', () => {
// Reset cache config mock
cacheConfig.USE_REDIS = false;
cacheConfig.REDIS_KEY_PREFIX = 'test';
cacheConfig.FORCED_IN_MEMORY_CACHE_NAMESPACES = [];
});
describe('redisCache', () => {
@@ -116,6 +118,30 @@ describe('cacheFactory', () => {
expect(mockKeyv).toHaveBeenCalledWith({ namespace: undefined, ttl: undefined });
});
it('should use fallback when namespace is in FORCED_IN_MEMORY_CACHE_NAMESPACES', () => {
cacheConfig.USE_REDIS = true;
cacheConfig.FORCED_IN_MEMORY_CACHE_NAMESPACES = ['forced-memory'];
const namespace = 'forced-memory';
const ttl = 3600;
standardCache(namespace, ttl);
expect(require('@keyv/redis').default).not.toHaveBeenCalled();
expect(mockKeyv).toHaveBeenCalledWith({ namespace, ttl });
});
it('should use Redis when namespace is not in FORCED_IN_MEMORY_CACHE_NAMESPACES', () => {
cacheConfig.USE_REDIS = true;
cacheConfig.FORCED_IN_MEMORY_CACHE_NAMESPACES = ['other-namespace'];
const namespace = 'test-namespace';
const ttl = 3600;
standardCache(namespace, ttl);
expect(require('@keyv/redis').default).toHaveBeenCalledWith(mockKeyvRedisClient);
expect(mockKeyv).toHaveBeenCalledWith(mockKeyvRedis, { namespace, ttl });
});
});
describe('violationCache', () => {

View File

@@ -33,6 +33,7 @@ const namespaces = {
[CacheKeys.ROLES]: standardCache(CacheKeys.ROLES),
[CacheKeys.MCP_TOOLS]: standardCache(CacheKeys.MCP_TOOLS),
[CacheKeys.CONFIG_STORE]: standardCache(CacheKeys.CONFIG_STORE),
[CacheKeys.STATIC_CONFIG]: standardCache(CacheKeys.STATIC_CONFIG),
[CacheKeys.PENDING_REQ]: standardCache(CacheKeys.PENDING_REQ),
[CacheKeys.ENCODED_DOMAINS]: new Keyv({ store: keyvMongo, namespace: CacheKeys.ENCODED_DOMAINS }),
[CacheKeys.ABORT_KEYS]: standardCache(CacheKeys.ABORT_KEYS, Time.TEN_MINUTES),

View File

@@ -1,6 +1,7 @@
const IoRedis = require('ioredis');
const { cacheConfig } = require('./cacheConfig');
const { logger } = require('@librechat/data-schemas');
const { createClient, createCluster } = require('@keyv/redis');
const { cacheConfig } = require('./cacheConfig');
const GLOBAL_PREFIX_SEPARATOR = '::';
@@ -25,17 +26,37 @@ if (cacheConfig.USE_REDIS) {
? new IoRedis(cacheConfig.REDIS_URI, redisOptions)
: new IoRedis.Cluster(cacheConfig.REDIS_URI, { redisOptions });
// Pinging the Redis server every 5 minutes to keep the connection alive
const pingInterval = setInterval(() => ioredisClient.ping(), 5 * 60 * 1000);
ioredisClient.on('close', () => clearInterval(pingInterval));
ioredisClient.on('end', () => clearInterval(pingInterval));
ioredisClient.on('error', (err) => {
logger.error('ioredis client error:', err);
});
/** Ping Interval to keep the Redis server connection alive (if enabled) */
let pingInterval = null;
const clearPingInterval = () => {
if (pingInterval) {
clearInterval(pingInterval);
pingInterval = null;
}
};
if (cacheConfig.REDIS_PING_INTERVAL > 0) {
pingInterval = setInterval(() => {
if (ioredisClient && ioredisClient.status === 'ready') {
ioredisClient.ping();
}
}, cacheConfig.REDIS_PING_INTERVAL * 1000);
ioredisClient.on('close', clearPingInterval);
ioredisClient.on('end', clearPingInterval);
}
}
/** @type {import('@keyv/redis').RedisClient | import('@keyv/redis').RedisCluster | null} */
let keyvRedisClient = null;
if (cacheConfig.USE_REDIS) {
// ** WARNING ** Keyv Redis client does not support Prefix like ioredis above.
// The prefix feature will be handled by the Keyv-Redis store in cacheFactory.js
/**
* ** WARNING ** Keyv Redis client does not support Prefix like ioredis above.
* The prefix feature will be handled by the Keyv-Redis store in cacheFactory.js
*/
const redisOptions = { username, password, socket: { tls: ca != null, ca } };
keyvRedisClient =
@@ -48,10 +69,28 @@ if (cacheConfig.USE_REDIS) {
keyvRedisClient.setMaxListeners(cacheConfig.REDIS_MAX_LISTENERS);
// Pinging the Redis server every 5 minutes to keep the connection alive
const keyvPingInterval = setInterval(() => keyvRedisClient.ping(), 5 * 60 * 1000);
keyvRedisClient.on('disconnect', () => clearInterval(keyvPingInterval));
keyvRedisClient.on('end', () => clearInterval(keyvPingInterval));
keyvRedisClient.on('error', (err) => {
logger.error('@keyv/redis client error:', err);
});
/** Ping Interval to keep the Redis server connection alive (if enabled) */
let pingInterval = null;
const clearPingInterval = () => {
if (pingInterval) {
clearInterval(pingInterval);
pingInterval = null;
}
};
if (cacheConfig.REDIS_PING_INTERVAL > 0) {
pingInterval = setInterval(() => {
if (keyvRedisClient && keyvRedisClient.isReady) {
keyvRedisClient.ping();
}
}, cacheConfig.REDIS_PING_INTERVAL * 1000);
keyvRedisClient.on('disconnect', clearPingInterval);
keyvRedisClient.on('end', clearPingInterval);
}
}
module.exports = { ioredisClient, keyvRedisClient, GLOBAL_PREFIX_SEPARATOR };

View File

@@ -52,7 +52,8 @@
"@librechat/agents": "^2.4.68",
"@librechat/api": "*",
"@librechat/data-schemas": "*",
"@node-saml/passport-saml": "^5.0.0",
"@modelcontextprotocol/sdk": "^1.17.0",
"@node-saml/passport-saml": "^5.1.0",
"@waylaidwanderer/fetch-event-source": "^3.0.1",
"axios": "^1.8.2",
"bcryptjs": "^2.4.3",
@@ -119,7 +120,7 @@
},
"devDependencies": {
"jest": "^29.7.0",
"mongodb-memory-server": "^10.1.3",
"mongodb-memory-server": "^10.1.4",
"nodemon": "^3.0.3",
"supertest": "^7.1.0"
}

View File

@@ -106,6 +106,7 @@ router.get('/', async function (req, res) {
const serverConfig = config.mcpServers[serverName];
payload.mcpServers[serverName] = {
customUserVars: serverConfig?.customUserVars || {},
chatMenu: serverConfig?.chatMenu,
};
}
}

View File

@@ -331,7 +331,8 @@ router.post('/:serverName/reinitialize', requireJwtAuth, async (req, res) => {
logger.info(`[MCP Reinitialize] Reinitializing server: ${serverName}`);
const config = await loadCustomConfig();
const printConfig = false;
const config = await loadCustomConfig(printConfig);
if (!config || !config.mcpServers || !config.mcpServers[serverName]) {
return res.status(404).json({
error: `MCP server '${serverName}' not found in configuration`,

View File

@@ -11,8 +11,8 @@ const getLogStores = require('~/cache/getLogStores');
* @returns {Promise<TCustomConfig | null>}
* */
async function getCustomConfig() {
const cache = getLogStores(CacheKeys.CONFIG_STORE);
return (await cache.get(CacheKeys.CUSTOM_CONFIG)) || (await loadCustomConfig());
const cache = getLogStores(CacheKeys.STATIC_CONFIG);
return (await cache.get(CacheKeys.LIBRECHAT_YAML_CONFIG)) || (await loadCustomConfig());
}
/**

View File

@@ -120,8 +120,8 @@ https://www.librechat.ai/docs/configuration/stt_tts`);
.forEach((endpoint) => parseCustomParams(endpoint.name, endpoint.customParams));
if (customConfig.cache) {
const cache = getLogStores(CacheKeys.CONFIG_STORE);
await cache.set(CacheKeys.CUSTOM_CONFIG, customConfig);
const cache = getLogStores(CacheKeys.STATIC_CONFIG);
await cache.set(CacheKeys.LIBRECHAT_YAML_CONFIG, customConfig);
}
if (result.data.modelSpecs) {

BIN
bun.lockb

Binary file not shown.

View File

@@ -34,6 +34,7 @@
"@dicebear/collection": "^9.2.2",
"@dicebear/core": "^9.2.2",
"@headlessui/react": "^2.1.2",
"@librechat/client": "*",
"@marsidev/react-turnstile": "^1.1.0",
"@radix-ui/react-accordion": "^1.1.2",
"@radix-ui/react-alert-dialog": "^1.0.2",
@@ -70,6 +71,7 @@
"i18next": "^24.2.2",
"i18next-browser-languagedetector": "^8.0.3",
"input-otp": "^1.4.2",
"jotai": "^2.12.5",
"js-cookie": "^3.0.5",
"librechat-data-provider": "*",
"lodash": "^4.17.21",

View File

@@ -4,10 +4,9 @@ import { RouterProvider } from 'react-router-dom';
import * as RadixToast from '@radix-ui/react-toast';
import { HTML5Backend } from 'react-dnd-html5-backend';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
import { Toast, ThemeProvider, ToastProvider } from '@librechat/client';
import { QueryClient, QueryClientProvider, QueryCache } from '@tanstack/react-query';
import { ScreenshotProvider, ThemeProvider, useApiErrorBoundary } from './hooks';
import { ToastProvider } from './Providers';
import Toast from './components/ui/Toast';
import { ScreenshotProvider, useApiErrorBoundary } from './hooks';
import { LiveAnnouncer } from '~/a11y';
import { router } from './routes';

View File

@@ -1,11 +1,9 @@
export { default as AssistantsProvider } from './AssistantsContext';
export { default as AgentsProvider } from './AgentsContext';
export { default as ToastProvider } from './ToastContext';
export * from './ActivePanelContext';
export * from './AgentPanelContext';
export * from './ChatContext';
export * from './ShareContext';
export * from './ToastContext';
export * from './FileMapContext';
export * from './AddedChatContext';
export * from './EditorContext';

View File

@@ -1,8 +1,7 @@
// client/src/a11y/LiveAnnouncer.tsx
import React, { useState, useCallback, useRef, useEffect, useMemo } from 'react';
import type { AnnounceOptions } from '~/common';
import AnnouncerContext from '~/Providers/AnnouncerContext';
import useLocalize from '~/hooks/useLocalize';
import { useLocalize } from '~/hooks';
import Announcer from './Announcer';
interface LiveAnnouncerProps {

View File

@@ -1,4 +1,3 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
export type RenderProp<
P = React.HTMLAttributes<any> & {
ref?: React.Ref<any>;

View File

@@ -206,7 +206,9 @@ export type AgentPanelProps = {
setActivePanel: React.Dispatch<React.SetStateAction<Panel>>;
setMcp: React.Dispatch<React.SetStateAction<t.MCP | undefined>>;
setAction: React.Dispatch<React.SetStateAction<t.Action | undefined>>;
endpointsConfig?: t.TEndpointsConfig;
setCurrentAgentId: React.Dispatch<React.SetStateAction<string | undefined>>;
agentsConfig?: t.TAgentsEndpoint | null;
};
export type AgentPanelContextType = {
@@ -217,14 +219,12 @@ export type AgentPanelContextType = {
mcps?: t.MCP[];
setMcp: React.Dispatch<React.SetStateAction<t.MCP | undefined>>;
setMcps: React.Dispatch<React.SetStateAction<t.MCP[] | undefined>>;
groupedTools: Record<string, t.AgentToolType & { tools?: t.AgentToolType[] }>;
tools: t.AgentToolType[];
activePanel?: string;
setActivePanel: React.Dispatch<React.SetStateAction<Panel>>;
setCurrentAgentId: React.Dispatch<React.SetStateAction<string | undefined>>;
groupedTools?: Record<string, t.AgentToolType & { tools?: t.AgentToolType[] }>;
agent_id?: string;
agentsConfig?: t.TAgentsEndpoint | null;
endpointsConfig?: t.TEndpointsConfig | null;
};
export type AgentModelPanelProps = {
@@ -336,16 +336,13 @@ export type TAskProps = {
export type TOptions = {
editedMessageId?: string | null;
editedText?: string | null;
editedContent?: {
index: number;
text: string;
type: 'text' | 'think';
};
isRegenerate?: boolean;
isContinued?: boolean;
isEdited?: boolean;
overrideMessages?: t.TMessage[];
/** Currently only utilized when resubmitting user-created message, uses that message's currently attached files */
/** This value is only true when the user submits a message with "Save & Submit" for a user-created message */
isResubmission?: boolean;
/** Currently only utilized when `isResubmission === true`, uses that message's currently attached files */
overrideFiles?: t.TMessage['files'];
};

View File

@@ -6,9 +6,9 @@ import type { SandpackPreviewRef, CodeEditorRef } from '@codesandbox/sandpack-re
import useArtifacts from '~/hooks/Artifacts/useArtifacts';
import DownloadArtifact from './DownloadArtifact';
import { useEditorContext } from '~/Providers';
import useLocalize from '~/hooks/useLocalize';
import ArtifactTabs from './ArtifactTabs';
import { CopyCodeButton } from './Code';
import { useLocalize } from '~/hooks';
import store from '~/store';
export default function Artifacts() {

View File

@@ -1,12 +1,11 @@
import React, { memo, useEffect, useRef, useState } from 'react';
import copy from 'copy-to-clipboard';
import rehypeKatex from 'rehype-katex';
import ReactMarkdown from 'react-markdown';
import rehypeHighlight from 'rehype-highlight';
import copy from 'copy-to-clipboard';
import { Clipboard, CheckMark } from '@librechat/client';
import { handleDoubleClick, langSubset } from '~/utils';
import Clipboard from '~/components/svg/Clipboard';
import CheckMark from '~/components/svg/CheckMark';
import useLocalize from '~/hooks/useLocalize';
import { useLocalize } from '~/hooks';
type TCodeProps = {
inline: boolean;

View File

@@ -1,9 +1,9 @@
import React, { useState } from 'react';
import { Download } from 'lucide-react';
import type { Artifact } from '~/common';
import { CheckMark } from '@librechat/client';
import useArtifactProps from '~/hooks/Artifacts/useArtifactProps';
import { useEditorContext } from '~/Providers';
import { CheckMark } from '~/components/svg';
import { useLocalize } from '~/hooks';
const DownloadArtifact = ({

View File

@@ -1,8 +1,7 @@
import React, { useEffect, useRef, useState } from 'react';
import mermaid from 'mermaid';
import { Button } from '@librechat/client';
import { TransformWrapper, TransformComponent, ReactZoomPanPinchRef } from 'react-zoom-pan-pinch';
// import { Button } from '/components/ui/Button'; // Live component
import { Button } from '~/components/ui/Button';
import { ZoomIn, ZoomOut, RefreshCw } from 'lucide-react';
interface MermaidDiagramProps {

View File

@@ -2,8 +2,8 @@
import { useEffect } from 'react';
import { useRecoilValue } from 'recoil';
import type { TMessageAudio } from '~/common';
import { VolumeIcon, VolumeMuteIcon, Spinner } from '@librechat/client';
import { useLocalize, useTTSBrowser, useTTSExternal } from '~/hooks';
import { VolumeIcon, VolumeMuteIcon, Spinner } from '~/components';
import { logger } from '~/utils';
import store from '~/store';

View File

@@ -1,8 +1,8 @@
import React from 'react';
import { useRecoilState } from 'recoil';
import { Dropdown } from '@librechat/client';
import type { Option } from '~/common';
import { useLocalize, useTTSBrowser, useTTSExternal } from '~/hooks';
import { Dropdown } from '~/components/ui';
import { logger } from '~/utils';
import store from '~/store';

View File

@@ -1,9 +1,9 @@
import { TranslationKeys, useLocalize } from '~/hooks';
import { ThemeSelector } from '@librechat/client';
import { TStartupConfig } from 'librechat-data-provider';
import { ErrorMessage } from '~/components/Auth/ErrorMessage';
import { TranslationKeys, useLocalize } from '~/hooks';
import SocialLoginRender from './SocialLoginRender';
import { BlinkAnimation } from './BlinkAnimation';
import { ThemeSelector } from '~/components';
import { Banner } from '../Banners';
import Footer from './Footer';

View File

@@ -1,10 +1,10 @@
import { useOutletContext, useSearchParams } from 'react-router-dom';
import { useEffect, useState } from 'react';
import { useAuthContext } from '~/hooks/AuthContext';
import { OpenIDIcon } from '@librechat/client';
import type { TLoginLayoutContext } from '~/common';
import { ErrorMessage } from '~/components/Auth/ErrorMessage';
import SocialButton from '~/components/Auth/SocialButton';
import { OpenIDIcon } from '~/components';
import { useAuthContext } from '~/hooks/AuthContext';
import { getLoginError } from '~/utils';
import { useLocalize } from '~/hooks';
import LoginForm from './LoginForm';

View File

@@ -1,11 +1,11 @@
import { useForm } from 'react-hook-form';
import React, { useState, useEffect, useContext } from 'react';
import { useForm } from 'react-hook-form';
import { Turnstile } from '@marsidev/react-turnstile';
import { ThemeContext, Spinner, Button } from '@librechat/client';
import type { TLoginUser, TStartupConfig } from 'librechat-data-provider';
import type { TAuthContext } from '~/common';
import { useResendVerificationEmail, useGetStartupConfig } from '~/data-provider';
import { ThemeContext, useLocalize } from '~/hooks';
import { Spinner, Button } from '~/components';
import { useLocalize } from '~/hooks';
type TLoginFormProps = {
onSubmit: (data: TLoginUser) => void;

View File

@@ -1,12 +1,12 @@
import { useForm } from 'react-hook-form';
import React, { useContext, useState } from 'react';
import { Turnstile } from '@marsidev/react-turnstile';
import { ThemeContext, Spinner, Button } from '@librechat/client';
import { useNavigate, useOutletContext, useLocation } from 'react-router-dom';
import { useRegisterUserMutation } from 'librechat-data-provider/react-query';
import type { TRegisterUser, TError } from 'librechat-data-provider';
import { useLocalize, TranslationKeys, ThemeContext } from '~/hooks';
import type { TLoginLayoutContext } from '~/common';
import { Spinner, Button } from '~/components';
import { useLocalize, TranslationKeys } from '~/hooks';
import { ErrorMessage } from './ErrorMessage';
const Registration: React.FC = () => {

View File

@@ -1,11 +1,11 @@
import { useForm } from 'react-hook-form';
import { useState, ReactNode } from 'react';
import { Spinner, Button } from '@librechat/client';
import { useOutletContext } from 'react-router-dom';
import { useRequestPasswordResetMutation } from 'librechat-data-provider/react-query';
import type { TRequestPasswordReset, TRequestPasswordResetResponse } from 'librechat-data-provider';
import type { FC } from 'react';
import type { TLoginLayoutContext } from '~/common';
import { Spinner, Button } from '~/components';
import type { FC } from 'react';
import { useLocalize } from '~/hooks';
const BodyTextWrapper: FC<{ children: ReactNode }> = ({ children }) => {

View File

@@ -1,10 +1,10 @@
import { useForm } from 'react-hook-form';
import { Spinner, Button } from '@librechat/client';
import { useOutletContext } from 'react-router-dom';
import { useNavigate, useSearchParams } from 'react-router-dom';
import { useResetPasswordMutation } from 'librechat-data-provider/react-query';
import type { TResetPassword } from 'librechat-data-provider';
import type { TLoginLayoutContext } from '~/common';
import { Spinner, Button } from '~/components';
import { useLocalize } from '~/hooks';
function ResetPassword() {

View File

@@ -6,7 +6,7 @@ import {
DiscordIcon,
AppleIcon,
SamlIcon,
} from '~/components';
} from '@librechat/client';
import SocialButton from './SocialButton';

View File

@@ -1,10 +1,10 @@
import React, { useState, useCallback } from 'react';
import { useSearchParams } from 'react-router-dom';
import { useToastContext } from '@librechat/client';
import { useForm, Controller } from 'react-hook-form';
import { REGEXP_ONLY_DIGITS, REGEXP_ONLY_DIGITS_AND_CHARS } from 'input-otp';
import { InputOTP, InputOTPGroup, InputOTPSeparator, InputOTPSlot, Label } from '~/components';
import { InputOTP, InputOTPGroup, InputOTPSeparator, InputOTPSlot, Label } from '@librechat/client';
import { useVerifyTwoFactorTempMutation } from '~/data-provider';
import { useToastContext } from '~/Providers';
import { useLocalize } from '~/hooks';
interface VerifyPayload {

View File

@@ -1,8 +1,7 @@
import { useSearchParams, useNavigate } from 'react-router-dom';
import { useState, useEffect, useMemo, useCallback } from 'react';
import { Spinner, ThemeSelector } from '@librechat/client';
import { useSearchParams, useNavigate } from 'react-router-dom';
import { useVerifyEmailMutation, useResendVerificationEmail } from '~/data-provider';
import { ThemeSelector } from '~/components/ui';
import { Spinner } from '~/components/svg';
import { useLocalize } from '~/hooks';
function RequestPasswordReset() {

View File

@@ -1,10 +1,8 @@
import React, { useRef, Dispatch, SetStateAction } from 'react';
import { TConversationTag } from 'librechat-data-provider';
import OGDialogTemplate from '~/components/ui/OGDialogTemplate';
import { OGDialogTemplate, OGDialog, Button, Spinner, useToastContext } from '@librechat/client';
import { useConversationTagMutation } from '~/data-provider';
import { OGDialog, Button, Spinner } from '~/components';
import { NotificationSeverity } from '~/common';
import { useToastContext } from '~/Providers';
import BookmarkForm from './BookmarkForm';
import { useLocalize } from '~/hooks';
import { logger } from '~/utils';

View File

@@ -2,11 +2,10 @@ import React, { useEffect } from 'react';
import { QueryKeys } from 'librechat-data-provider';
import { Controller, useForm } from 'react-hook-form';
import { useQueryClient } from '@tanstack/react-query';
import { Checkbox, Label, TextareaAutosize, Input, useToastContext } from '@librechat/client';
import type { TConversationTag, TConversationTagRequest } from 'librechat-data-provider';
import { Checkbox, Label, TextareaAutosize, Input } from '~/components';
import { useBookmarkContext } from '~/Providers/BookmarkContext';
import { useConversationTagMutation } from '~/data-provider';
import { useToastContext } from '~/Providers';
import { useLocalize } from '~/hooks';
import { cn, logger } from '~/utils';

View File

@@ -1,8 +1,8 @@
import { useState } from 'react';
import { Spinner } from '@librechat/client';
import { MenuItem } from '@headlessui/react';
import { BookmarkFilledIcon, BookmarkIcon } from '@radix-ui/react-icons';
import type { FC } from 'react';
import { Spinner } from '~/components/svg';
type MenuItemProps = {
tag: string | React.ReactNode;

View File

@@ -1,10 +1,17 @@
import { useCallback, useState } from 'react';
import {
Button,
TrashIcon,
Label,
OGDialog,
OGDialogTrigger,
TooltipAnchor,
OGDialogTemplate,
useToastContext,
} from '@librechat/client';
import type { FC } from 'react';
import { Button, TrashIcon, Label, OGDialog, OGDialogTrigger, TooltipAnchor } from '~/components';
import { useDeleteConversationTagMutation } from '~/data-provider';
import OGDialogTemplate from '~/components/ui/OGDialogTemplate';
import { NotificationSeverity } from '~/common';
import { useToastContext } from '~/Providers';
import { useLocalize } from '~/hooks';
const DeleteBookmarkButton: FC<{

View File

@@ -1,7 +1,7 @@
import { useState } from 'react';
import type { FC } from 'react';
import { TooltipAnchor, OGDialogTrigger, EditIcon, Button } from '@librechat/client';
import type { TConversationTag } from 'librechat-data-provider';
import { TooltipAnchor, OGDialogTrigger, EditIcon, Button } from '~/components';
import type { FC } from 'react';
import BookmarkEditDialog from './BookmarkEditDialog';
import { useLocalize } from '~/hooks';

View File

@@ -1,8 +1,8 @@
import { PlusCircle } from 'lucide-react';
import { TooltipAnchor } from '@librechat/client';
import { isAssistantsEndpoint } from 'librechat-data-provider';
import type { TConversation } from 'librechat-data-provider';
import { useChatContext, useAddedChatContext } from '~/Providers';
import { TooltipAnchor } from '~/components';
import { mainTextareaId } from '~/common';
import { useLocalize } from '~/hooks';

View File

@@ -1,6 +1,7 @@
import { memo, useCallback } from 'react';
import { useRecoilValue } from 'recoil';
import { useForm } from 'react-hook-form';
import { Spinner } from '@librechat/client';
import { useParams } from 'react-router-dom';
import { Constants } from 'librechat-data-provider';
import type { TMessage } from 'librechat-data-provider';
@@ -10,7 +11,6 @@ import { useChatHelpers, useAddedResponse, useSSE } from '~/hooks';
import ConversationStarters from './Input/ConversationStarters';
import { useGetMessagesByConvoId } from '~/data-provider';
import MessagesView from './Messages/MessagesView';
import { Spinner } from '~/components/svg';
import Presentation from './Presentation';
import { buildTree, cn } from '~/utils';
import ChatForm from './Input/ChatForm';

View File

@@ -2,11 +2,11 @@ import { useState, useId, useRef } from 'react';
import { useRecoilValue } from 'recoil';
import * as Ariakit from '@ariakit/react';
import { Upload, Share2 } from 'lucide-react';
import { DropdownPopup, TooltipAnchor, useMediaQuery } from '@librechat/client';
import type * as t from '~/common';
import ExportModal from '~/components/Nav/ExportConversation/ExportModal';
import { ShareButton } from '~/components/Conversations/ConvoOptions';
import { DropdownPopup, TooltipAnchor } from '~/components/ui';
import { useMediaQuery, useLocalize } from '~/hooks';
import { useLocalize } from '~/hooks';
import store from '~/store';
export default function ExportAndShareMenu({

View File

@@ -1,4 +1,5 @@
import { useMemo } from 'react';
import { useMediaQuery } from '@librechat/client';
import { useOutletContext } from 'react-router-dom';
import { getConfigDefaults, PermissionTypes, Permissions } from 'librechat-data-provider';
import type { ContextType } from '~/common';
@@ -6,10 +7,10 @@ import ModelSelector from './Menus/Endpoints/ModelSelector';
import { PresetsMenu, HeaderNewChat, OpenSidebar } from './Menus';
import { useGetStartupConfig } from '~/data-provider';
import ExportAndShareMenu from './ExportAndShareMenu';
import { useMediaQuery, useHasAccess } from '~/hooks';
import BookmarkMenu from './Menus/BookmarkMenu';
import { TemporaryChat } from './TemporaryChat';
import AddMultiConvo from './AddMultiConvo';
import { useHasAccess } from '~/hooks';
const defaultInterface = getConfigDefaults().interface;

View File

@@ -1,8 +1,8 @@
import React, { memo, useState, useCallback, useMemo } from 'react';
import * as Ariakit from '@ariakit/react';
import { CheckboxButton } from '@librechat/client';
import { ArtifactModes } from 'librechat-data-provider';
import { WandSparkles, ChevronDown } from 'lucide-react';
import CheckboxButton from '~/components/ui/CheckboxButton';
import { useBadgeRowContext } from '~/Providers';
import { useLocalize } from '~/hooks';
import { cn } from '~/utils';

View File

@@ -1,8 +1,8 @@
import React from 'react';
import * as Ariakit from '@ariakit/react';
import { PinIcon } from '@librechat/client';
import { ChevronRight, WandSparkles } from 'lucide-react';
import { ArtifactModes } from 'librechat-data-provider';
import { PinIcon } from '~/components/svg';
import { useLocalize } from '~/hooks';
import { cn } from '~/utils';

View File

@@ -1,8 +1,7 @@
import { useCallback } from 'react';
import { useChatFormContext, useToastContext } from '~/Providers';
import { ListeningIcon, Spinner } from '~/components/svg';
import { useToastContext, TooltipAnchor, ListeningIcon, Spinner } from '@librechat/client';
import { useLocalize, useSpeechToText } from '~/hooks';
import { TooltipAnchor } from '~/components/ui';
import { useChatFormContext } from '~/Providers';
import { globalAudioId } from '~/common';
import { cn } from '~/utils';

View File

@@ -8,6 +8,7 @@ import React, {
useReducer,
useCallback,
} from 'react';
import { Badge } from '@librechat/client';
import { useRecoilValue, useRecoilCallback } from 'recoil';
import type { LucideIcon } from 'lucide-react';
import CodeInterpreter from './CodeInterpreter';
@@ -15,7 +16,6 @@ import { BadgeRowProvider } from '~/Providers';
import ToolsDropdown from './ToolsDropdown';
import type { BadgeItem } from '~/common';
import { useChatBadges } from '~/hooks';
import { Badge } from '~/components/ui';
import ToolDialogs from './ToolDialogs';
import FileSearch from './FileSearch';
import Artifacts from './Artifacts';

View File

@@ -1,5 +1,6 @@
import { memo, useRef, useMemo, useEffect, useState, useCallback } from 'react';
import { useWatch } from 'react-hook-form';
import { TextareaAutosize } from '@librechat/client';
import { useRecoilState, useRecoilValue } from 'recoil';
import { Constants, isAssistantsEndpoint, isAgentsEndpoint } from 'librechat-data-provider';
import {
@@ -20,7 +21,6 @@ import {
import { mainTextareaId, BadgeItem } from '~/common';
import AttachFileChat from './Files/AttachFileChat';
import FileFormChat from './Files/FileFormChat';
import { TextareaAutosize } from '~/components';
import { cn, removeFocusRings } from '~/utils';
import TextareaHeader from './TextareaHeader';
import PromptsCommand from './PromptsCommand';

View File

@@ -1,6 +1,6 @@
import React from 'react';
import { CircleIcon, CircleDotsIcon } from '~/components/svg';
import { ECallState } from 'librechat-data-provider';
import { CircleIcon, CircleDotsIcon } from '@librechat/client';
const CircleRender = ({ rmsLevel, isCameraOn, state }) => {
const getIconComponent = (state) => {

View File

@@ -1,7 +1,7 @@
import React, { memo } from 'react';
import { TerminalSquareIcon } from 'lucide-react';
import { CheckboxButton } from '@librechat/client';
import { PermissionTypes, Permissions } from 'librechat-data-provider';
import CheckboxButton from '~/components/ui/CheckboxButton';
import { useLocalize, useHasAccess } from '~/hooks';
import { useBadgeRowContext } from '~/Providers';

View File

@@ -1,6 +1,6 @@
import React from 'react';
import { TooltipAnchor } from '@librechat/client';
import { ChevronDown, ChevronUp } from 'lucide-react';
import { TooltipAnchor } from '~/components/ui';
import { useLocalize } from '~/hooks';
import { cn } from '~/utils';

View File

@@ -1,9 +1,9 @@
import React, { useCallback } from 'react';
import { Edit3, Check, X } from 'lucide-react';
import { Button, Badge } from '@librechat/client';
import type { LucideIcon } from 'lucide-react';
import type { BadgeItem } from '~/common';
import { useChatBadges, useLocalize } from '~/hooks';
import { Button, Badge } from '~/components/ui';
interface EditBadgesProps {
isEditingChatBadges: boolean;

View File

@@ -1,9 +1,8 @@
import React, { memo } from 'react';
import { CheckboxButton, VectorIcon } from '@librechat/client';
import { PermissionTypes, Permissions } from 'librechat-data-provider';
import CheckboxButton from '~/components/ui/CheckboxButton';
import { useLocalize, useHasAccess } from '~/hooks';
import { useBadgeRowContext } from '~/Providers';
import { VectorIcon } from '~/components/svg';
function FileSearch() {
const localize = useLocalize();

View File

@@ -1,5 +1,5 @@
import React, { useRef } from 'react';
import { FileUpload, TooltipAnchor, AttachmentIcon } from '~/components';
import { FileUpload, TooltipAnchor, AttachmentIcon } from '@librechat/client';
import { useLocalize, useFileHandling } from '~/hooks';
import { cn } from '~/utils';

View File

@@ -1,11 +1,11 @@
import { useSetRecoilState } from 'recoil';
import * as Ariakit from '@ariakit/react';
import React, { useRef, useState, useMemo } from 'react';
import * as Ariakit from '@ariakit/react';
import { useSetRecoilState } from 'recoil';
import { FileSearch, ImageUpIcon, TerminalSquareIcon, FileType2Icon } from 'lucide-react';
import { FileUpload, TooltipAnchor, DropdownPopup, AttachmentIcon } from '@librechat/client';
import { EToolResources, EModelEndpoint, defaultAgentCapabilities } from 'librechat-data-provider';
import type { EndpointFileConfig } from 'librechat-data-provider';
import { useLocalize, useGetAgentsConfig, useFileHandling, useAgentCapabilities } from '~/hooks';
import { FileUpload, TooltipAnchor, DropdownPopup, AttachmentIcon } from '~/components';
import { ephemeralAgentByConvoId } from '~/store';
import { cn } from '~/utils';

View File

@@ -1,8 +1,8 @@
import React, { useMemo } from 'react';
import { OGDialog, OGDialogTemplate } from '@librechat/client';
import { ImageUpIcon, FileSearch, TerminalSquareIcon, FileType2Icon } from 'lucide-react';
import { EToolResources, defaultAgentCapabilities } from 'librechat-data-provider';
import { useLocalize, useGetAgentsConfig, useAgentCapabilities } from '~/hooks';
import { OGDialog, OGDialogTemplate } from '~/components/ui';
interface DragDropModalProps {
onOptionSelect: (option: EToolResources | undefined) => void;

View File

@@ -1,7 +1,7 @@
import { Spinner } from '@librechat/client';
import type { TFile } from 'librechat-data-provider';
import type { ExtendedFile } from '~/common';
import FileIcon from '~/components/svg/Files/FileIcon';
import { Spinner } from '~/components';
import { FileIcon } from '~/components/svg';
import SourceIcon from './SourceIcon';
import { cn } from '~/utils';

View File

@@ -1,11 +1,11 @@
import { useEffect } from 'react';
import { useToastContext } from '@librechat/client';
import { EToolResources } from 'librechat-data-provider';
import type { ExtendedFile } from '~/common';
import { useDeleteFilesMutation } from '~/data-provider';
import { useToastContext } from '~/Providers';
import { useLocalize } from '~/hooks';
import { useFileDeletion } from '~/hooks/Files';
import FileContainer from './FileContainer';
import { useLocalize } from '~/hooks';
import { logger } from '~/utils';
import Image from './Image';

View File

@@ -1,6 +1,6 @@
import { FileSources, FileContext } from 'librechat-data-provider';
import type { TFile } from 'librechat-data-provider';
import { OGDialog, OGDialogContent, OGDialogHeader, OGDialogTitle } from '~/components';
import { OGDialog, OGDialogContent, OGDialogHeader, OGDialogTitle } from '@librechat/client';
import { useGetFiles } from '~/data-provider';
import { DataTable, columns } from './Table';
import { useLocalize } from '~/hooks';

View File

@@ -1,7 +1,7 @@
import { useState, useEffect, useCallback } from 'react';
import { Maximize2 } from 'lucide-react';
import { OGDialog, OGDialogContent } from '~/components/ui';
import { FileSources } from 'librechat-data-provider';
import { OGDialog, OGDialogContent } from '@librechat/client';
import ProgressCircle from './ProgressCircle';
import SourceIcon from './SourceIcon';
import { cn } from '~/utils';
@@ -93,9 +93,9 @@ const ImagePreview = ({
const style: styleProps = imageUrl
? {
...baseStyle,
backgroundImage: `url(${imageUrl})`,
}
...baseStyle,
backgroundImage: `url(${imageUrl})`,
}
: baseStyle;
if (typeof style.backgroundImage !== 'string' || style.backgroundImage.length === 0) {

View File

@@ -1,14 +1,19 @@
/* eslint-disable react-hooks/rules-of-hooks */
import { ArrowUpDown, Database } from 'lucide-react';
import { FileSources, FileContext } from 'librechat-data-provider';
import {
Button,
Checkbox,
OpenAIMinimalIcon,
AzureMinimalIcon,
useMediaQuery,
} from '@librechat/client';
import type { ColumnDef } from '@tanstack/react-table';
import type { TFile } from 'librechat-data-provider';
import { Button, Checkbox, OpenAIMinimalIcon, AzureMinimalIcon } from '~/components';
import ImagePreview from '~/components/Chat/Input/Files/ImagePreview';
import FilePreview from '~/components/Chat/Input/Files/FilePreview';
import { TranslationKeys, useLocalize } from '~/hooks';
import { SortFilterHeader } from './SortFilterHeader';
import { TranslationKeys, useLocalize, useMediaQuery } from '~/hooks';
import { formatDate, getFileType } from '~/utils';
const contextMap: Record<any, TranslationKeys> = {

View File

@@ -16,8 +16,6 @@ import type {
ColumnFiltersState,
} from '@tanstack/react-table';
import { FileContext } from 'librechat-data-provider';
import type { AugmentedColumnDef } from '~/common';
import type { TFile } from 'librechat-data-provider';
import {
Button,
Input,
@@ -31,11 +29,14 @@ import {
DropdownMenuCheckboxItem,
DropdownMenuContent,
DropdownMenuTrigger,
} from '~/components/ui';
TrashIcon,
Spinner,
useMediaQuery,
} from '@librechat/client';
import type { TFile } from 'librechat-data-provider';
import type { AugmentedColumnDef } from '~/common';
import { useDeleteFilesFromTable } from '~/hooks/Files';
import { TrashIcon, Spinner } from '~/components/svg';
import useLocalize from '~/hooks/useLocalize';
import { useMediaQuery } from '~/hooks';
import { useLocalize } from '~/hooks';
import { cn } from '~/utils';
import store from '~/store';

View File

@@ -2,13 +2,13 @@ import { Column } from '@tanstack/react-table';
import { ListFilter, FilterX } from 'lucide-react';
import { ArrowDownIcon, ArrowUpIcon, CaretSortIcon } from '@radix-ui/react-icons';
import {
Button,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from '~/components/ui/DropdownMenu';
import { Button } from '~/components/ui/Button';
} from '@librechat/client';
import { useLocalize, TranslationKeys } from '~/hooks';
import { cn } from '~/utils';

View File

@@ -1,4 +1,4 @@
import { DotsIcon, TrashIcon } from '~/components/svg';
import { DotsIcon, TrashIcon } from '@librechat/client';
export default function Template() {
return (

View File

@@ -2,12 +2,12 @@ import { useRecoilState } from 'recoil';
import { Settings2 } from 'lucide-react';
import { useState, useEffect, useMemo } from 'react';
import { Root, Anchor } from '@radix-ui/react-popover';
import { EModelEndpoint, isParamEndpoint, tConvoUpdateSchema } from 'librechat-data-provider';
import { PluginStoreDialog, TooltipAnchor } from '@librechat/client';
import { useUserKeyQuery } from 'librechat-data-provider/react-query';
import { EModelEndpoint, isParamEndpoint, tConvoUpdateSchema } from 'librechat-data-provider';
import type { TPreset, TInterfaceConfig } from 'librechat-data-provider';
import { EndpointSettings, SaveAsPresetDialog, AlternativeSettings } from '~/components/Endpoints';
import { useSetIndexOptions, useMediaQuery, useLocalize } from '~/hooks';
import { PluginStoreDialog, TooltipAnchor } from '~/components';
import { useSetIndexOptions, useLocalize } from '~/hooks';
import { useGetEndpointsQuery } from '~/data-provider';
import OptionsPopover from './OptionsPopover';
import PopoverButtons from './PopoverButtons';

View File

@@ -0,0 +1,121 @@
import React, { useEffect } from 'react';
import { useForm, Controller } from 'react-hook-form';
import { Button, Input, Label, OGDialog, OGDialogTemplate } from '@librechat/client';
import { useLocalize } from '~/hooks';
export interface ConfigFieldDetail {
title: string;
description: string;
}
interface MCPConfigDialogProps {
isOpen: boolean;
onOpenChange: (isOpen: boolean) => void;
fieldsSchema: Record<string, ConfigFieldDetail>;
initialValues: Record<string, string>;
onSave: (updatedValues: Record<string, string>) => void;
isSubmitting?: boolean;
onRevoke?: () => void;
serverName: string;
}
export default function MCPConfigDialog({
isOpen,
onOpenChange,
fieldsSchema,
initialValues,
onSave,
isSubmitting = false,
onRevoke,
serverName,
}: MCPConfigDialogProps) {
const localize = useLocalize();
const {
control,
handleSubmit,
reset,
formState: { errors, _ },
} = useForm<Record<string, string>>({
defaultValues: initialValues,
});
useEffect(() => {
if (isOpen) {
reset(initialValues);
}
}, [isOpen, initialValues, reset]);
const onFormSubmit = (data: Record<string, string>) => {
onSave(data);
};
const handleRevoke = () => {
if (onRevoke) {
onRevoke();
}
};
const dialogTitle = localize('com_ui_configure_mcp_variables_for', { 0: serverName });
const dialogDescription = localize('com_ui_mcp_dialog_desc');
return (
<OGDialog open={isOpen} onOpenChange={onOpenChange}>
<OGDialogTemplate
className="sm:max-w-lg"
title={dialogTitle}
description={dialogDescription}
headerClassName="px-6 pt-6 pb-4"
main={
<form onSubmit={handleSubmit(onFormSubmit)} className="space-y-4 px-6 pb-2">
{Object.entries(fieldsSchema).map(([key, details]) => (
<div key={key} className="space-y-2">
<Label htmlFor={key} className="text-sm font-medium">
{details.title}
</Label>
<Controller
name={key}
control={control}
defaultValue={initialValues[key] || ''}
render={({ field }) => (
<Input
id={key}
type="text"
{...field}
placeholder={localize('com_ui_mcp_enter_var', { 0: details.title })}
className="w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 dark:border-gray-600 dark:bg-gray-700 dark:text-white sm:text-sm"
/>
)}
/>
{details.description && (
<p
className="text-xs text-text-secondary [&_a]:text-blue-500 [&_a]:hover:text-blue-600 dark:[&_a]:text-blue-400 dark:[&_a]:hover:text-blue-300"
dangerouslySetInnerHTML={{ __html: details.description }}
/>
)}
{errors[key] && <p className="text-xs text-red-500">{errors[key]?.message}</p>}
</div>
))}
</form>
}
selection={{
selectHandler: handleSubmit(onFormSubmit),
selectClasses: 'bg-green-500 hover:bg-green-600 text-white',
selectText: isSubmitting ? localize('com_ui_saving') : localize('com_ui_save'),
}}
buttons={
onRevoke && (
<Button
onClick={handleRevoke}
className="bg-red-600 text-white hover:bg-red-700 dark:hover:bg-red-800"
disabled={isSubmitting}
>
{localize('com_ui_revoke')}
</Button>
)
}
footerClassName="flex justify-end gap-2 px-6 pb-6 pt-2"
showCancelButton={true}
/>
</OGDialog>
);
}

View File

@@ -1,109 +1,20 @@
import { useQueryClient } from '@tanstack/react-query';
import { Constants, QueryKeys } from 'librechat-data-provider';
import type { TUpdateUserPlugins, TPlugin } from 'librechat-data-provider';
import React, { memo, useCallback, useState, useMemo, useRef } from 'react';
import { useUpdateUserPluginsMutation } from 'librechat-data-provider/react-query';
import MCPConfigDialog, { ConfigFieldDetail } from '~/components/ui/MCP/MCPConfigDialog';
import { useMCPServerInitialization } from '~/hooks/MCP/useMCPServerInitialization';
import React, { memo, useCallback } from 'react';
import { MultiSelect, MCPIcon } from '@librechat/client';
import MCPServerStatusIcon from '~/components/ui/MCP/MCPServerStatusIcon';
import { useToastContext, useBadgeRowContext } from '~/Providers';
import MultiSelect from '~/components/ui/MultiSelect';
import { MCPIcon } from '~/components/svg';
import { useLocalize } from '~/hooks';
import { useMCPServerManager } from '~/hooks/MCP/useMCPServerManager';
import MCPConfigDialog from '~/components/ui/MCP/MCPConfigDialog';
function MCPSelect() {
const localize = useLocalize();
const { showToast } = useToastContext();
const { mcpSelect, startupConfig } = useBadgeRowContext();
const { mcpValues, setMCPValues, mcpToolDetails, isPinned } = mcpSelect;
// Get all configured MCP servers from config
const configuredServers = useMemo(() => {
return Object.keys(startupConfig?.mcpServers || {});
}, [startupConfig?.mcpServers]);
const [isConfigModalOpen, setIsConfigModalOpen] = useState(false);
const [selectedToolForConfig, setSelectedToolForConfig] = useState<TPlugin | null>(null);
const previousFocusRef = useRef<HTMLElement | null>(null);
const queryClient = useQueryClient();
const updateUserPluginsMutation = useUpdateUserPluginsMutation({
onSuccess: async () => {
showToast({ message: localize('com_nav_mcp_vars_updated'), status: 'success' });
// tools so we dont leave tools available for use in chat if we revoke and thus kill mcp server
// auth values so customUserVars flags are updated in customUserVarsSection
// connection status so connection indicators are updated in the dropdown
await Promise.all([
queryClient.refetchQueries([QueryKeys.tools]),
queryClient.refetchQueries([QueryKeys.mcpAuthValues]),
queryClient.refetchQueries([QueryKeys.mcpConnectionStatus]),
]);
},
onError: (error: unknown) => {
console.error('Error updating MCP auth:', error);
showToast({
message: localize('com_nav_mcp_vars_update_error'),
status: 'error',
});
},
});
// Use the shared initialization hook
const { initializeServer, isInitializing, connectionStatus, cancelOAuthFlow, isCancellable } =
useMCPServerInitialization({
onSuccess: (serverName) => {
// Add to selected values after successful initialization
const currentValues = mcpValues ?? [];
if (!currentValues.includes(serverName)) {
setMCPValues([...currentValues, serverName]);
}
},
onError: (serverName) => {
// Find the tool/server configuration
const tool = mcpToolDetails?.find((t) => t.name === serverName);
const serverConfig = startupConfig?.mcpServers?.[serverName];
const serverStatus = connectionStatus[serverName];
// Check if this server would show a config button
const hasAuthConfig =
(tool?.authConfig && tool.authConfig.length > 0) ||
(serverConfig?.customUserVars && Object.keys(serverConfig.customUserVars).length > 0);
// Only open dialog if the server would have shown a config button
// (disconnected/error states always show button, connected only shows if hasAuthConfig)
const wouldShowButton =
!serverStatus ||
serverStatus.connectionState === 'disconnected' ||
serverStatus.connectionState === 'error' ||
(serverStatus.connectionState === 'connected' && hasAuthConfig);
if (!wouldShowButton) {
return; // Don't open dialog if no button would be shown
}
// Create tool object if it doesn't exist
const configTool = tool || {
name: serverName,
pluginKey: `${Constants.mcp_prefix}${serverName}`,
authConfig: serverConfig?.customUserVars
? Object.entries(serverConfig.customUserVars).map(([key, config]) => ({
authField: key,
label: config.title,
description: config.description,
}))
: [],
authenticated: false,
};
previousFocusRef.current = document.activeElement as HTMLElement;
// Open the config dialog on error
setSelectedToolForConfig(configTool);
setIsConfigModalOpen(true);
},
});
const {
configuredServers,
mcpValues,
isPinned,
placeholderText,
batchToggleServers,
getServerStatusIconProps,
getConfigDialogProps,
localize,
} = useMCPServerManager();
const renderSelectedValues = useCallback(
(values: string[], placeholder?: string) => {
@@ -118,137 +29,9 @@ function MCPSelect() {
[localize],
);
const handleConfigSave = useCallback(
(targetName: string, authData: Record<string, string>) => {
if (selectedToolForConfig && selectedToolForConfig.name === targetName) {
// Use the pluginKey directly since it's already in the correct format
console.log(
`[MCP Select] Saving config for ${targetName}, pluginKey: ${`${Constants.mcp_prefix}${targetName}`}`,
);
const payload: TUpdateUserPlugins = {
pluginKey: `${Constants.mcp_prefix}${targetName}`,
action: 'install',
auth: authData,
};
updateUserPluginsMutation.mutate(payload);
}
},
[selectedToolForConfig, updateUserPluginsMutation],
);
const handleConfigRevoke = useCallback(
(targetName: string) => {
if (selectedToolForConfig && selectedToolForConfig.name === targetName) {
// Use the pluginKey directly since it's already in the correct format
const payload: TUpdateUserPlugins = {
pluginKey: `${Constants.mcp_prefix}${targetName}`,
action: 'uninstall',
auth: {},
};
updateUserPluginsMutation.mutate(payload);
// Remove the server from selected values after revoke
const currentValues = mcpValues ?? [];
const filteredValues = currentValues.filter((name) => name !== targetName);
setMCPValues(filteredValues);
}
},
[selectedToolForConfig, updateUserPluginsMutation, mcpValues, setMCPValues],
);
const handleSave = useCallback(
(authData: Record<string, string>) => {
if (selectedToolForConfig) {
handleConfigSave(selectedToolForConfig.name, authData);
}
},
[selectedToolForConfig, handleConfigSave],
);
const handleRevoke = useCallback(() => {
if (selectedToolForConfig) {
handleConfigRevoke(selectedToolForConfig.name);
}
}, [selectedToolForConfig, handleConfigRevoke]);
const handleDialogOpenChange = useCallback((open: boolean) => {
setIsConfigModalOpen(open);
// Restore focus when dialog closes
if (!open && previousFocusRef.current) {
// Use setTimeout to ensure the dialog has fully closed before restoring focus
setTimeout(() => {
if (previousFocusRef.current && typeof previousFocusRef.current.focus === 'function') {
previousFocusRef.current.focus();
}
previousFocusRef.current = null;
}, 0);
}
}, []);
// Get connection status for all MCP servers (now from hook)
// Remove the duplicate useMCPConnectionStatusQuery since it's in the hook
// Modified setValue function that attempts to initialize disconnected servers
const filteredSetMCPValues = useCallback(
(values: string[]) => {
// Separate connected and disconnected servers
const connectedServers: string[] = [];
const disconnectedServers: string[] = [];
values.forEach((serverName) => {
const serverStatus = connectionStatus[serverName];
if (serverStatus?.connectionState === 'connected') {
connectedServers.push(serverName);
} else {
disconnectedServers.push(serverName);
}
});
// Only set connected servers as selected values
setMCPValues(connectedServers);
// Attempt to initialize each disconnected server (once)
disconnectedServers.forEach((serverName) => {
initializeServer(serverName);
});
},
[connectionStatus, setMCPValues, initializeServer],
);
const renderItemContent = useCallback(
(serverName: string, defaultContent: React.ReactNode) => {
const tool = mcpToolDetails?.find((t) => t.name === serverName);
const serverStatus = connectionStatus[serverName];
const serverConfig = startupConfig?.mcpServers?.[serverName];
const handleConfigClick = (e: React.MouseEvent) => {
e.stopPropagation();
e.preventDefault();
previousFocusRef.current = document.activeElement as HTMLElement;
const configTool = tool || {
name: serverName,
pluginKey: `${Constants.mcp_prefix}${serverName}`,
authConfig: serverConfig?.customUserVars
? Object.entries(serverConfig.customUserVars).map(([key, config]) => ({
authField: key,
label: config.title,
description: config.description,
}))
: [],
authenticated: false,
};
setSelectedToolForConfig(configTool);
setIsConfigModalOpen(true);
};
const handleCancelClick = (e: React.MouseEvent) => {
e.stopPropagation();
e.preventDefault();
cancelOAuthFlow(serverName);
};
const statusIconProps = getServerStatusIconProps(serverName);
// Common wrapper for the main content (check mark + text)
// Ensures Check & Text are adjacent and the group takes available space.
@@ -262,22 +45,7 @@ function MCPSelect() {
</button>
);
// Check if this server has customUserVars to configure
const hasCustomUserVars =
serverConfig?.customUserVars && Object.keys(serverConfig.customUserVars).length > 0;
const statusIcon = (
<MCPServerStatusIcon
serverName={serverName}
serverStatus={serverStatus}
tool={tool}
onConfigClick={handleConfigClick}
isInitializing={isInitializing(serverName)}
canCancel={isCancellable(serverName)}
onCancel={handleCancelClick}
hasCustomUserVars={hasCustomUserVars}
/>
);
const statusIcon = statusIconProps && <MCPServerStatusIcon {...statusIconProps} />;
if (statusIcon) {
return (
@@ -290,14 +58,7 @@ function MCPSelect() {
return mainContentWrapper;
},
[
isInitializing,
isCancellable,
mcpToolDetails,
cancelOAuthFlow,
connectionStatus,
startupConfig?.mcpServers,
],
[getServerStatusIconProps],
);
// Don't render if no servers are selected and not pinned
@@ -310,14 +71,14 @@ function MCPSelect() {
return null;
}
const placeholderText =
startupConfig?.interface?.mcpServers?.placeholder || localize('com_ui_mcp_servers');
const configDialogProps = getConfigDialogProps();
return (
<>
<MultiSelect
items={configuredServers}
selectedValues={mcpValues ?? []}
setSelectedValues={filteredSetMCPValues}
setSelectedValues={batchToggleServers}
defaultSelectedValues={mcpValues ?? []}
renderSelectedValues={renderSelectedValues}
renderItemContent={renderItemContent}
@@ -328,39 +89,7 @@ function MCPSelect() {
selectItemsClassName="border border-blue-600/50 bg-blue-500/10 hover:bg-blue-700/10"
selectClassName="group relative inline-flex items-center justify-center md:justify-start gap-1.5 rounded-full border border-border-medium text-sm font-medium transition-all md:w-full size-9 p-2 md:p-3 bg-transparent shadow-sm hover:bg-surface-hover hover:shadow-md active:shadow-inner"
/>
{selectedToolForConfig && (
<MCPConfigDialog
serverName={selectedToolForConfig.name}
serverStatus={connectionStatus[selectedToolForConfig.name]}
isOpen={isConfigModalOpen}
onOpenChange={handleDialogOpenChange}
fieldsSchema={(() => {
const schema: Record<string, ConfigFieldDetail> = {};
if (selectedToolForConfig?.authConfig) {
selectedToolForConfig.authConfig.forEach((field) => {
schema[field.authField] = {
title: field.label,
description: field.description,
};
});
}
return schema;
})()}
initialValues={(() => {
const initial: Record<string, string> = {};
// Note: Actual initial values might need to be fetched if they are stored user-specifically
if (selectedToolForConfig?.authConfig) {
selectedToolForConfig.authConfig.forEach((field) => {
initial[field.authField] = ''; // Or fetched value
});
}
return initial;
})()}
onSave={handleSave}
onRevoke={handleRevoke}
isSubmitting={updateUserPluginsMutation.isLoading}
/>
)}
{configDialogProps && <MCPConfigDialog {...configDialogProps} />}
</>
);
}

View File

@@ -1,29 +1,27 @@
import React from 'react';
import * as Ariakit from '@ariakit/react';
import { ChevronRight } from 'lucide-react';
import { PinIcon, MCPIcon } from '~/components/svg';
import { useLocalize } from '~/hooks';
import { PinIcon, MCPIcon } from '@librechat/client';
import MCPServerStatusIcon from '~/components/ui/MCP/MCPServerStatusIcon';
import { useMCPServerManager } from '~/hooks/MCP/useMCPServerManager';
import MCPConfigDialog from '~/components/ui/MCP/MCPConfigDialog';
import { cn } from '~/utils';
interface MCPSubMenuProps {
isMCPPinned: boolean;
setIsMCPPinned: (value: boolean) => void;
mcpValues?: string[];
mcpServerNames: string[];
handleMCPToggle: (serverName: string) => void;
placeholder?: string;
}
const MCPSubMenu = ({
mcpValues,
isMCPPinned,
mcpServerNames,
setIsMCPPinned,
handleMCPToggle,
placeholder,
...props
}: MCPSubMenuProps) => {
const localize = useLocalize();
const MCPSubMenu = ({ placeholder, ...props }: MCPSubMenuProps) => {
const {
configuredServers,
mcpValues,
isPinned,
setIsPinned,
placeholderText,
toggleServerSelection,
getServerStatusIconProps,
getConfigDialogProps,
} = useMCPServerManager();
const menuStore = Ariakit.useMenuStore({
focusLoop: true,
@@ -31,72 +29,96 @@ const MCPSubMenu = ({
placement: 'right',
});
// Don't render if no MCP servers are configured
if (!configuredServers || configuredServers.length === 0) {
return null;
}
const configDialogProps = getConfigDialogProps();
return (
<Ariakit.MenuProvider store={menuStore}>
<Ariakit.MenuItem
{...props}
render={
<Ariakit.MenuButton
onClick={(e: React.MouseEvent<HTMLButtonElement>) => {
e.stopPropagation();
menuStore.toggle();
}}
className="flex w-full cursor-pointer items-center justify-between rounded-lg p-2 hover:bg-surface-hover"
/>
}
>
<div className="flex items-center gap-2">
<MCPIcon className="icon-md" />
<span>{placeholder || localize('com_ui_mcp_servers')}</span>
<ChevronRight className="ml-auto h-3 w-3" />
</div>
<button
type="button"
onClick={(e) => {
e.stopPropagation();
setIsMCPPinned(!isMCPPinned);
}}
className={cn(
'rounded p-1 transition-all duration-200',
'hover:bg-surface-tertiary hover:shadow-sm',
!isMCPPinned && 'text-text-secondary hover:text-text-primary',
)}
aria-label={isMCPPinned ? 'Unpin' : 'Pin'}
<>
<Ariakit.MenuProvider store={menuStore}>
<Ariakit.MenuItem
{...props}
render={
<Ariakit.MenuButton
onClick={(e: React.MouseEvent<HTMLButtonElement>) => {
e.stopPropagation();
menuStore.toggle();
}}
className="flex w-full cursor-pointer items-center justify-between rounded-lg p-2 hover:bg-surface-hover"
/>
}
>
<div className="h-4 w-4">
<PinIcon unpin={isMCPPinned} />
<div className="flex items-center gap-2">
<MCPIcon className="icon-md" />
<span>{placeholder || placeholderText}</span>
<ChevronRight className="ml-auto h-3 w-3" />
</div>
</button>
</Ariakit.MenuItem>
<Ariakit.Menu
portal={true}
unmountOnHide={true}
className={cn(
'animate-popover-left z-50 ml-3 flex min-w-[200px] flex-col rounded-xl',
'border border-border-light bg-surface-secondary p-1 shadow-lg',
)}
>
{mcpServerNames.map((serverName) => (
<Ariakit.MenuItem
key={serverName}
onClick={(event) => {
event.preventDefault();
handleMCPToggle(serverName);
<button
type="button"
onClick={(e) => {
e.stopPropagation();
setIsPinned(!isPinned);
}}
className={cn(
'flex items-center gap-2 rounded-lg px-2 py-1.5 text-text-primary hover:cursor-pointer',
'scroll-m-1 outline-none transition-colors',
'hover:bg-black/[0.075] dark:hover:bg-white/10',
'data-[active-item]:bg-black/[0.075] dark:data-[active-item]:bg-white/10',
'w-full min-w-0 text-sm',
'rounded p-1 transition-all duration-200',
'hover:bg-surface-tertiary hover:shadow-sm',
!isPinned && 'text-text-secondary hover:text-text-primary',
)}
aria-label={isPinned ? 'Unpin' : 'Pin'}
>
<Ariakit.MenuItemCheck checked={mcpValues?.includes(serverName) ?? false} />
<span>{serverName}</span>
</Ariakit.MenuItem>
))}
</Ariakit.Menu>
</Ariakit.MenuProvider>
<div className="h-4 w-4">
<PinIcon unpin={isPinned} />
</div>
</button>
</Ariakit.MenuItem>
<Ariakit.Menu
portal={true}
unmountOnHide={true}
className={cn(
'animate-popover-left z-50 ml-3 flex min-w-[200px] flex-col rounded-xl',
'border border-border-light bg-surface-secondary p-1 shadow-lg',
)}
>
{configuredServers.map((serverName) => {
const statusIconProps = getServerStatusIconProps(serverName);
const isSelected = mcpValues?.includes(serverName) ?? false;
const statusIcon = statusIconProps && <MCPServerStatusIcon {...statusIconProps} />;
return (
<Ariakit.MenuItem
key={serverName}
onClick={(event) => {
event.preventDefault();
toggleServerSelection(serverName);
}}
className={cn(
'flex items-center gap-2 rounded-lg px-2 py-1.5 text-text-primary hover:cursor-pointer',
'scroll-m-1 outline-none transition-colors',
'hover:bg-black/[0.075] dark:hover:bg-white/10',
'data-[active-item]:bg-black/[0.075] dark:data-[active-item]:bg-white/10',
'w-full min-w-0 justify-between text-sm',
)}
>
<button
type="button"
className="flex flex-grow items-center gap-2 rounded bg-transparent p-0 text-left transition-colors focus:outline-none"
tabIndex={0}
>
<Ariakit.MenuItemCheck checked={isSelected} />
<span>{serverName}</span>
</button>
{statusIcon && <div className="ml-2 flex items-center">{statusIcon}</div>}
</Ariakit.MenuItem>
);
})}
</Ariakit.Menu>
</Ariakit.MenuProvider>
{configDialogProps && <MCPConfigDialog {...configDialogProps} />}
</>
);
};

View File

@@ -1,12 +1,13 @@
import { useState, useRef, useEffect } from 'react';
import { useCombobox } from '@librechat/client';
import { AutoSizer, List } from 'react-virtualized';
import { EModelEndpoint } from 'librechat-data-provider';
import type { SetterOrUpdater } from 'recoil';
import type { MentionOption, ConvoGenerator } from '~/common';
import type { SetterOrUpdater } from 'recoil';
import useSelectMention from '~/hooks/Input/useSelectMention';
import { useLocalize, TranslationKeys } from '~/hooks';
import { useAssistantsMapContext } from '~/Providers';
import useMentions from '~/hooks/Input/useMentions';
import { useLocalize, useCombobox, TranslationKeys } from '~/hooks';
import { removeCharIfLast } from '~/utils';
import MentionItem from './MentionItem';

View File

@@ -1,11 +1,10 @@
import { useRef } from 'react';
import { Save } from 'lucide-react';
import { Portal, Content } from '@radix-ui/react-popover';
import { Button, CrossIcon, useOnClickOutside } from '@librechat/client';
import type { ReactNode } from 'react';
import { useLocalize, useOnClickOutside } from '~/hooks';
import { cn, removeFocusOutlines } from '~/utils';
import { CrossIcon } from '~/components/svg';
import { Button } from '~/components/ui';
import { useLocalize } from '~/hooks';
type TOptionsPopoverProps = {
children: ReactNode;

View File

@@ -1,9 +1,8 @@
import { useRecoilState } from 'recoil';
import { EModelEndpoint, SettingsViews } from 'librechat-data-provider';
import { Button, MessagesSquared, GPTIcon, AssistantIcon, DataIcon } from '@librechat/client';
import type { ReactNode } from 'react';
import { MessagesSquared, GPTIcon, AssistantIcon, DataIcon } from '~/components/svg';
import { useChatContext } from '~/Providers';
import { Button } from '~/components/ui';
import { useLocalize } from '~/hooks';
import { cn } from '~/utils/';
import store from '~/store';

View File

@@ -1,5 +1,6 @@
import { useState, useRef, useEffect, useMemo, memo, useCallback } from 'react';
import { AutoSizer, List } from 'react-virtualized';
import { Spinner, useCombobox } from '@librechat/client';
import { useSetRecoilState, useRecoilValue } from 'recoil';
import { PermissionTypes, Permissions } from 'librechat-data-provider';
import type { TPromptGroup } from 'librechat-data-provider';
@@ -7,9 +8,8 @@ import type { PromptOption } from '~/common';
import { removeCharIfLast, mapPromptGroups, detectVariables } from '~/utils';
import VariableDialog from '~/components/Prompts/Groups/VariableDialog';
import CategoryIcon from '~/components/Prompts/Groups/CategoryIcon';
import { useLocalize, useCombobox, useHasAccess } from '~/hooks';
import { useLocalize, useHasAccess } from '~/hooks';
import { useGetAllPromptGroups } from '~/data-provider';
import { Spinner } from '~/components/svg';
import MentionItem from './MentionItem';
import store from '~/store';

View File

@@ -1,8 +1,7 @@
import React, { forwardRef } from 'react';
import { useWatch } from 'react-hook-form';
import type { Control } from 'react-hook-form';
import { TooltipAnchor } from '~/components/ui';
import { SendIcon } from '~/components/svg';
import { SendIcon, TooltipAnchor } from '@librechat/client';
import { useLocalize } from '~/hooks';
import { cn } from '~/utils';

View File

@@ -1,4 +1,4 @@
import { TooltipAnchor } from '~/components/ui';
import { TooltipAnchor } from '@librechat/client';
import { useLocalize } from '~/hooks';
import { cn } from '~/utils';

View File

@@ -1,6 +1,7 @@
import React, { useState, useMemo, useCallback } from 'react';
import * as Ariakit from '@ariakit/react';
import { Globe, Settings, Settings2, TerminalSquareIcon } from 'lucide-react';
import { TooltipAnchor, DropdownPopup, PinIcon, VectorIcon } from '@librechat/client';
import type { MenuItemProps } from '~/common';
import {
AuthType,
@@ -9,11 +10,9 @@ import {
PermissionTypes,
defaultAgentCapabilities,
} from 'librechat-data-provider';
import { TooltipAnchor, DropdownPopup } from '~/components';
import { useLocalize, useHasAccess, useAgentCapabilities } from '~/hooks';
import ArtifactsSubMenu from '~/components/Chat/Input/ArtifactsSubMenu';
import MCPSubMenu from '~/components/Chat/Input/MCPSubMenu';
import { PinIcon, VectorIcon } from '~/components/svg';
import { useBadgeRowContext } from '~/Providers';
import { cn } from '~/utils';
@@ -55,12 +54,7 @@ const ToolsDropdown = ({ disabled }: ToolsDropdownProps) => {
} = codeInterpreter;
const { isPinned: isFileSearchPinned, setIsPinned: setIsFileSearchPinned } = fileSearch;
const { isPinned: isArtifactsPinned, setIsPinned: setIsArtifactsPinned } = artifacts;
const {
mcpValues,
mcpServerNames,
isPinned: isMCPPinned,
setIsPinned: setIsMCPPinned,
} = mcpSelect;
const { mcpServerNames } = mcpSelect;
const canUseWebSearch = useHasAccess({
permissionType: PermissionTypes.WEB_SEARCH,
@@ -130,17 +124,6 @@ const ToolsDropdown = ({ disabled }: ToolsDropdownProps) => {
}
}, [artifacts]);
const handleMCPToggle = useCallback(
(serverName: string) => {
const currentValues = mcpSelect.mcpValues ?? [];
const newValues = currentValues.includes(serverName)
? currentValues.filter((v) => v !== serverName)
: [...currentValues, serverName];
mcpSelect.setMCPValues(newValues);
},
[mcpSelect],
);
const mcpPlaceholder = startupConfig?.interface?.mcpServers?.placeholder;
const dropdownItems: MenuItemProps[] = [];
@@ -305,17 +288,7 @@ const ToolsDropdown = ({ disabled }: ToolsDropdownProps) => {
if (mcpServerNames && mcpServerNames.length > 0) {
dropdownItems.push({
hideOnClick: false,
render: (props) => (
<MCPSubMenu
{...props}
mcpValues={mcpValues}
isMCPPinned={isMCPPinned}
placeholder={mcpPlaceholder}
mcpServerNames={mcpServerNames}
setIsMCPPinned={setIsMCPPinned}
handleMCPToggle={handleMCPToggle}
/>
),
render: (props) => <MCPSubMenu {...props} placeholder={mcpPlaceholder} />,
});
}
@@ -343,7 +316,7 @@ const ToolsDropdown = ({ disabled }: ToolsDropdownProps) => {
return (
<DropdownPopup
itemClassName="flex w-full cursor-pointer items-center justify-between hover:bg-surface-hover gap-5"
itemClassName="flex w-full cursor-pointer rounded-lg items-center justify-between hover:bg-surface-hover gap-5"
menuId="tools-dropdown-menu"
isOpen={isPopoverActive}
setIsOpen={setIsPopoverActive}

View File

@@ -1,7 +1,7 @@
import React, { memo } from 'react';
import { Globe } from 'lucide-react';
import { CheckboxButton } from '@librechat/client';
import { Permissions, PermissionTypes } from 'librechat-data-provider';
import CheckboxButton from '~/components/ui/CheckboxButton';
import { useLocalize, useHasAccess } from '~/hooks';
import { useBadgeRowContext } from '~/Providers';

View File

@@ -1,9 +1,9 @@
import { useMemo, useCallback, useState, useEffect, useRef } from 'react';
import { easings } from '@react-spring/web';
import { EModelEndpoint } from 'librechat-data-provider';
import { BirthdayIcon, TooltipAnchor, SplitText } from '@librechat/client';
import { useChatContext, useAgentsMapContext, useAssistantsMapContext } from '~/Providers';
import { useGetEndpointsQuery, useGetStartupConfig } from '~/data-provider';
import { BirthdayIcon, TooltipAnchor, SplitText } from '~/components';
import ConvoIcon from '~/components/Endpoints/ConvoIcon';
import { useLocalize, useAuthContext } from '~/hooks';
import { getIconEndpoint, getEntity } from '~/utils';

View File

@@ -5,17 +5,15 @@ import { BookmarkPlusIcon } from 'lucide-react';
import { useQueryClient } from '@tanstack/react-query';
import { Constants, QueryKeys } from 'librechat-data-provider';
import { BookmarkFilledIcon, BookmarkIcon } from '@radix-ui/react-icons';
import { DropdownPopup, TooltipAnchor, Spinner, useToastContext } from '@librechat/client';
import type { TConversationTag } from 'librechat-data-provider';
import type { FC } from 'react';
import type * as t from '~/common';
import { useConversationTagsQuery, useTagConversationMutation } from '~/data-provider';
import { DropdownPopup, TooltipAnchor } from '~/components/ui';
import { BookmarkContext } from '~/Providers/BookmarkContext';
import { BookmarkEditDialog } from '~/components/Bookmarks';
import { useBookmarkSuccess, useLocalize } from '~/hooks';
import { NotificationSeverity } from '~/common';
import { useToastContext } from '~/Providers';
import { Spinner } from '~/components';
import { cn, logger } from '~/utils';
import store from '~/store';

View File

@@ -1,8 +1,8 @@
import React, { useState } from 'react';
import { BookmarkPlusIcon } from 'lucide-react';
import { OGDialogTrigger } from '@librechat/client';
import type { FC } from 'react';
import { BookmarkEditDialog, BookmarkItems, BookmarkItem } from '~/components/Bookmarks';
import { OGDialogTrigger } from '~/components/ui';
import { useLocalize } from '~/hooks';
export const BookmarkMenuItems: FC<{

View File

@@ -1,11 +1,11 @@
import { useMemo } from 'react';
import { SettingsIcon } from 'lucide-react';
import { TooltipAnchor, Spinner } from '@librechat/client';
import { EModelEndpoint, isAgentsEndpoint, isAssistantsEndpoint } from 'librechat-data-provider';
import type { Endpoint } from '~/common';
import { CustomMenu as Menu, CustomMenuItem as MenuItem } from '../CustomMenu';
import { useModelSelectorContext } from '../ModelSelectorContext';
import { renderEndpointModels } from './EndpointModelItem';
import { TooltipAnchor, Spinner } from '~/components';
import { filterModels } from '../utils';
import { useLocalize } from '~/hooks';
import { cn } from '~/utils';

View File

@@ -1,8 +1,7 @@
import { useQueryClient } from '@tanstack/react-query';
import { QueryKeys, Constants } from 'librechat-data-provider';
import { TooltipAnchor, Button, NewChatIcon } from '@librechat/client';
import type { TMessage } from 'librechat-data-provider';
import { TooltipAnchor, Button } from '~/components/ui';
import { NewChatIcon } from '~/components/svg';
import { useChatContext } from '~/Providers';
import { useLocalize } from '~/hooks';

View File

@@ -1,5 +1,4 @@
import { TooltipAnchor, Button } from '~/components/ui';
import { Sidebar } from '~/components/svg';
import { TooltipAnchor, Button, Sidebar } from '@librechat/client';
import { useLocalize } from '~/hooks';
export default function OpenSidebar({

View File

@@ -2,6 +2,14 @@ import { useRecoilState } from 'recoil';
import { useCallback, useEffect, useMemo } from 'react';
import { useQueryClient } from '@tanstack/react-query';
import { QueryKeys, isAgentsEndpoint } from 'librechat-data-provider';
import {
Input,
Label,
OGDialog,
OGDialogTitle,
SelectDropDown,
OGDialogContent,
} from '@librechat/client';
import type { TModelsConfig, TEndpointsConfig } from 'librechat-data-provider';
import {
cn,
@@ -10,14 +18,6 @@ import {
mapEndpoints,
getConvoSwitchLogic,
} from '~/utils';
import {
Input,
Label,
OGDialog,
OGDialogTitle,
SelectDropDown,
OGDialogContent,
} from '~/components';
import { useSetIndexOptions, useLocalize, useDebouncedInput } from '~/hooks';
import PopoverButtons from '~/components/Chat/Input/PopoverButtons';
import { EndpointSettings } from '~/components/Endpoints';

View File

@@ -1,13 +1,19 @@
import { useRecoilValue } from 'recoil';
import { Close } from '@radix-ui/react-popover';
import { Flipper, Flipped } from 'react-flip-toolkit';
import type { FC } from 'react';
import {
Dialog,
DialogTrigger,
Label,
DialogTemplate,
PinIcon,
EditIcon,
TrashIcon,
} from '@librechat/client';
import type { TPreset } from 'librechat-data-provider';
import type { FC } from 'react';
import { getPresetTitle, getEndpointField, getIconKey } from '~/utils';
import FileUpload from '~/components/Chat/Input/Files/FileUpload';
import { PinIcon, EditIcon, TrashIcon } from '~/components/svg';
import { Dialog, DialogTrigger, Label } from '~/components/ui';
import DialogTemplate from '~/components/ui/DialogTemplate';
import { useGetEndpointsQuery } from '~/data-provider';
import { MenuSeparator, MenuItem } from '../UI';
import { icons } from '~/hooks/Endpoint/Icons';

View File

@@ -1,10 +1,10 @@
import type { FC } from 'react';
import { BookCopy } from 'lucide-react';
import { TooltipAnchor } from '@librechat/client';
import { Content, Portal, Root, Trigger } from '@radix-ui/react-popover';
import { EditPresetDialog, PresetItems } from './Presets';
import { useLocalize, usePresets } from '~/hooks';
import { useChatContext } from '~/Providers';
import { TooltipAnchor } from '~/components';
const PresetsMenu: FC = () => {
const localize = useLocalize();

View File

@@ -1,7 +1,7 @@
import { useState } from 'react';
import { ChevronDown } from 'lucide-react';
import { Trigger } from '@radix-ui/react-popover';
import useLocalize from '~/hooks/useLocalize';
import { useLocalize } from '~/hooks';
export default function TitleButton({ primaryText = '', secondaryText = '' }) {
const localize = useLocalize();

View File

@@ -11,9 +11,9 @@ import { ThinkingButton } from '~/components/Artifacts/Thinking';
import { MessageContext, SearchContext } from '~/Providers';
import MemoryArtifacts from './MemoryArtifacts';
import Sources from '~/components/Web/Sources';
import useLocalize from '~/hooks/useLocalize';
import { mapAttachments } from '~/utils/map';
import { EditTextPart } from './Parts';
import { useLocalize } from '~/hooks';
import store from '~/store';
import Part from './Part';

View File

@@ -1,6 +1,6 @@
import { useState, useEffect, useCallback, useRef } from 'react';
import { Button, OGDialog, OGDialogContent, TooltipAnchor } from '@librechat/client';
import { X, ArrowDownToLine, PanelLeftOpen, PanelLeftClose, RotateCcw } from 'lucide-react';
import { Button, OGDialog, OGDialogContent, TooltipAnchor } from '~/components';
import { useLocalize } from '~/hooks';
const getQualityStyles = (quality: string): string => {

View File

@@ -1,10 +1,10 @@
import { useRef, useEffect, useCallback } from 'react';
import { useRecoilState, useRecoilValue } from 'recoil';
import { useForm } from 'react-hook-form';
import { useRecoilState, useRecoilValue } from 'recoil';
import { TextareaAutosize, TooltipAnchor } from '@librechat/client';
import { useUpdateMessageMutation } from 'librechat-data-provider/react-query';
import type { TEditProps } from '~/common';
import { useChatContext, useAddedChatContext } from '~/Providers';
import { TextareaAutosize, TooltipAnchor } from '~/components/ui';
import { cn, removeFocusRings } from '~/utils';
import { useLocalize } from '~/hooks';
import Container from './Container';

View File

@@ -1,8 +1,8 @@
import React, { useState, useRef, useMemo } from 'react';
import { Skeleton } from '@librechat/client';
import { LazyLoadImage } from 'react-lazy-load-image-component';
import { cn, scaleImage } from '~/utils';
import DialogImage from './DialogImage';
import { Skeleton } from '~/components';
const Image = ({
imagePath,

View File

@@ -1,12 +1,13 @@
import React, { memo, useMemo, useRef, useEffect } from 'react';
import { useRecoilValue } from 'recoil';
import { useToastContext } from '@librechat/client';
import { PermissionTypes, Permissions } from 'librechat-data-provider';
import { useToastContext, useCodeBlockContext } from '~/Providers';
import CodeBlock from '~/components/Messages/Content/CodeBlock';
import useHasAccess from '~/hooks/Roles/useHasAccess';
import { useFileDownload } from '~/data-provider';
import useLocalize from '~/hooks/useLocalize';
import { useCodeBlockContext } from '~/Providers';
import { handleDoubleClick } from '~/utils';
import { useLocalize } from '~/hooks';
import store from '~/store';
type TCodeProps = {

View File

@@ -1,10 +1,10 @@
import { memo, Suspense, useMemo } from 'react';
import { useRecoilValue } from 'recoil';
import { DelayedRender } from '@librechat/client';
import type { TMessage } from 'librechat-data-provider';
import type { TMessageContentProps, TDisplayProps } from '~/common';
import Error from '~/components/Messages/Content/Error';
import Thinking from '~/components/Artifacts/Thinking';
import { DelayedRender } from '~/components/ui';
import { useChatContext } from '~/Providers';
import MarkdownLite from './MarkdownLite';
import EditMessage from './EditMessage';

View File

@@ -1,5 +1,6 @@
import { useRef, useEffect, useCallback, useMemo } from 'react';
import { useForm } from 'react-hook-form';
import { TextareaAutosize } from '@librechat/client';
import { ContentTypes } from 'librechat-data-provider';
import { useRecoilState, useRecoilValue } from 'recoil';
import { useUpdateMessageContentMutation } from 'librechat-data-provider/react-query';
@@ -7,7 +8,6 @@ import type { Agents } from 'librechat-data-provider';
import type { TEditProps } from '~/common';
import Container from '~/components/Chat/Messages/Content/Container';
import { useChatContext, useAddedChatContext } from '~/Providers';
import { TextareaAutosize } from '~/components/ui';
import { cn, removeFocusRings } from '~/utils';
import { useLocalize } from '~/hooks';
import store from '~/store';

View File

@@ -1,6 +1,6 @@
import React from 'react';
import { useToastContext } from '@librechat/client';
import { useCodeOutputDownload } from '~/data-provider';
import { useToastContext } from '~/Providers';
interface LogLinkProps {
href: string;

View File

@@ -1,8 +1,8 @@
import { useState, useEffect, useRef, useCallback } from 'react';
import { PixelCard } from '@librechat/client';
import type { TAttachment, TFile, TAttachmentMetadata } from 'librechat-data-provider';
import Image from '~/components/Chat/Messages/Content/Image';
import ProgressText from './ProgressText';
import { PixelCard } from '~/components';
import { scaleImage } from '~/utils';
export default function OpenAIImageGen({

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