Compare commits
68 Commits
feat/granu
...
feat/clien
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
014060a11b | ||
|
|
80d406b629 | ||
|
|
22035bbf95 | ||
|
|
17a164b420 | ||
|
|
5b663d0e35 | ||
|
|
c273dfc1f4 | ||
|
|
de69bcdd64 | ||
|
|
59412c2b36 | ||
|
|
0af8fba7ca | ||
|
|
d5cf83313b | ||
|
|
355da0fc2e | ||
|
|
8ba5aa6055 | ||
|
|
f1204531a8 | ||
|
|
a65e33758d | ||
|
|
0855631c54 | ||
|
|
a1e052871f | ||
|
|
ca18ada9e2 | ||
|
|
103af99879 | ||
|
|
412948e025 | ||
|
|
4da25826d9 | ||
|
|
6e7fdeb3a3 | ||
|
|
1c3f5b972d | ||
|
|
e7aa83e073 | ||
|
|
0b5155d277 | ||
|
|
86deb4d19a | ||
|
|
f741a59ec4 | ||
|
|
83477bba34 | ||
|
|
5476029bca | ||
|
|
05a0a1f7cd | ||
|
|
55f67212d5 | ||
|
|
63d0c301a0 | ||
|
|
bac6e499b7 | ||
|
|
59de92afa9 | ||
|
|
e297386cee | ||
|
|
e2b1cc607f | ||
|
|
55bda03d19 | ||
|
|
b11ba35790 | ||
|
|
8e1b00da2a | ||
|
|
39f5dd47dc | ||
|
|
16f83c6e8e | ||
|
|
b71a82d0e9 | ||
|
|
63a5902404 | ||
|
|
21c3a831c3 | ||
|
|
ae43b4eed0 | ||
|
|
2af4ca5b5c | ||
|
|
b6c7b0bc71 | ||
|
|
f8738b207c | ||
|
|
6ea1d5eab2 | ||
|
|
992911514c | ||
|
|
9289aeb2ba | ||
|
|
898d273aaf | ||
|
|
5859350bcb | ||
|
|
882cca247a | ||
|
|
0b7dd55797 | ||
|
|
9f270127d3 | ||
|
|
1380db85cb | ||
|
|
dcaa5af598 | ||
|
|
545a909953 | ||
|
|
cd436dc6a8 | ||
|
|
e75beb92b3 | ||
|
|
5251246313 | ||
|
|
26f23c6aaf | ||
|
|
1636af1f27 | ||
|
|
b050a0bf1e | ||
|
|
deb928bf80 | ||
|
|
21005b66cc | ||
|
|
3dc9e85fab | ||
|
|
ec67cf2d3a |
@@ -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 #
|
||||
#==================================================#
|
||||
|
||||
46
.github/workflows/client.yml
vendored
46
.github/workflows/client.yml
vendored
@@ -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
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
3
.github/workflows/i18n-unused-keys.yml
vendored
3
.github/workflows/i18n-unused-keys.yml
vendored
@@ -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
|
||||
|
||||
101
.github/workflows/unused-packages.yml
vendored
101
.github/workflows/unused-packages.yml
vendored
@@ -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
|
||||
|
||||
21
api/cache/cacheConfig.js
vendored
21
api/cache/cacheConfig.js
vendored
@@ -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),
|
||||
|
||||
49
api/cache/cacheConfig.spec.js
vendored
49
api/cache/cacheConfig.spec.js
vendored
@@ -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([]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
5
api/cache/cacheFactory.js
vendored
5
api/cache/cacheFactory.js
vendored
@@ -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;
|
||||
|
||||
26
api/cache/cacheFactory.spec.js
vendored
26
api/cache/cacheFactory.spec.js
vendored
@@ -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', () => {
|
||||
|
||||
1
api/cache/getLogStores.js
vendored
1
api/cache/getLogStores.js
vendored
@@ -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),
|
||||
|
||||
61
api/cache/redisClients.js
vendored
61
api/cache/redisClients.js
vendored
@@ -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 };
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -106,6 +106,7 @@ router.get('/', async function (req, res) {
|
||||
const serverConfig = config.mcpServers[serverName];
|
||||
payload.mcpServers[serverName] = {
|
||||
customUserVars: serverConfig?.customUserVars || {},
|
||||
chatMenu: serverConfig?.chatMenu,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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`,
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
export type RenderProp<
|
||||
P = React.HTMLAttributes<any> & {
|
||||
ref?: React.Ref<any>;
|
||||
|
||||
@@ -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'];
|
||||
};
|
||||
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 = ({
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 = () => {
|
||||
|
||||
@@ -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 }) => {
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
DiscordIcon,
|
||||
AppleIcon,
|
||||
SamlIcon,
|
||||
} from '~/components';
|
||||
} from '@librechat/client';
|
||||
|
||||
import SocialButton from './SocialButton';
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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<{
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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> = {
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { DotsIcon, TrashIcon } from '~/components/svg';
|
||||
import { DotsIcon, TrashIcon } from '@librechat/client';
|
||||
|
||||
export default function Template() {
|
||||
return (
|
||||
|
||||
@@ -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';
|
||||
|
||||
121
client/src/components/Chat/Input/MCPConfigDialog.tsx
Normal file
121
client/src/components/Chat/Input/MCPConfigDialog.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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} />}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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} />}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { TooltipAnchor } from '~/components/ui';
|
||||
import { TooltipAnchor } from '@librechat/client';
|
||||
import { useLocalize } from '~/hooks';
|
||||
import { cn } from '~/utils';
|
||||
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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<{
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user