diff --git a/api/cache/clearPendingReq.js b/api/cache/clearPendingReq.js index d31d51d78a..068711d311 100644 --- a/api/cache/clearPendingReq.js +++ b/api/cache/clearPendingReq.js @@ -1,29 +1,48 @@ -const Keyv = require('keyv'); -const { pendingReqFile } = require('./keyvFiles'); -const { LIMIT_CONCURRENT_MESSAGES } = process.env ?? {}; - -const keyv = new Keyv({ store: pendingReqFile, namespace: 'pendingRequests' }); +const getLogStores = require('./getLogStores'); +const { isEnabled } = require('../server/utils'); +const { USE_REDIS, LIMIT_CONCURRENT_MESSAGES } = process.env ?? {}; +const ttl = 1000 * 60 * 1; /** - * Clear pending requests from the cache. + * Clear or decrement pending requests from the cache. * Checks the environmental variable LIMIT_CONCURRENT_MESSAGES; - * if the rule is enabled ('true'), pending requests in the cache are cleared. + * if the rule is enabled ('true'), it either decrements the count of pending requests + * or deletes the key if the count is less than or equal to 1. * * @module clearPendingReq - * @requires keyv - * @requires keyvFiles + * @requires ./getLogStores + * @requires ../server/utils * @requires process * * @async * @function - * @returns {Promise} A promise that either clears 'pendingRequests' from store or resolves with no value. + * @param {Object} params - The parameters object. + * @param {string} params.userId - The user ID for which the pending requests are to be cleared or decremented. + * @param {Object} [params.cache] - An optional cache object to use. If not provided, a default cache will be fetched using getLogStores. + * @returns {Promise} A promise that either decrements the 'pendingRequests' count, deletes the key from the store, or resolves with no value. */ -const clearPendingReq = async () => { - if (LIMIT_CONCURRENT_MESSAGES?.toLowerCase() !== 'true') { +const clearPendingReq = async ({ userId, cache: _cache }) => { + if (!userId) { + return; + } else if (!isEnabled(LIMIT_CONCURRENT_MESSAGES)) { return; } - await keyv.clear(); + const namespace = 'pending_req'; + const cache = _cache ?? getLogStores(namespace); + + if (!cache) { + return; + } + + const key = `${USE_REDIS ? namespace : ''}:${userId ?? ''}`; + const currentReq = +((await cache.get(key)) ?? 0); + + if (currentReq && currentReq >= 1) { + await cache.set(key, currentReq - 1, ttl); + } else { + await cache.delete(key); + } }; module.exports = clearPendingReq; diff --git a/api/cache/getLogStores.js b/api/cache/getLogStores.js index 56839fcd2a..2692b6933c 100644 --- a/api/cache/getLogStores.js +++ b/api/cache/getLogStores.js @@ -1,26 +1,37 @@ const Keyv = require('keyv'); const keyvMongo = require('./keyvMongo'); -const { math } = require('../server/utils'); +const keyvRedis = require('./keyvRedis'); +const { math, isEnabled } = require('../server/utils'); const { logFile, violationFile } = require('./keyvFiles'); -const { BAN_DURATION } = process.env ?? {}; +const { BAN_DURATION, USE_REDIS } = process.env ?? {}; const duration = math(BAN_DURATION, 7200000); +const createViolationInstance = (namespace) => { + const config = isEnabled(USE_REDIS) ? { store: keyvRedis } : { store: violationFile, namespace }; + return new Keyv(config); +}; + +// Serve cache from memory so no need to clear it on startup/exit +const pending_req = isEnabled(USE_REDIS) + ? new Keyv({ store: keyvRedis }) + : new Keyv({ namespace: 'pending_req' }); + const namespaces = { - ban: new Keyv({ store: keyvMongo, ttl: duration, namespace: 'bans' }), + pending_req, + ban: new Keyv({ store: keyvMongo, namespace: 'bans', duration }), general: new Keyv({ store: logFile, namespace: 'violations' }), - concurrent: new Keyv({ store: violationFile, namespace: 'concurrent' }), - non_browser: new Keyv({ store: violationFile, namespace: 'non_browser' }), - message_limit: new Keyv({ store: violationFile, namespace: 'message_limit' }), - token_balance: new Keyv({ store: violationFile, namespace: 'token_balance' }), - registrations: new Keyv({ store: violationFile, namespace: 'registrations' }), - logins: new Keyv({ store: violationFile, namespace: 'logins' }), + concurrent: createViolationInstance('concurrent'), + non_browser: createViolationInstance('non_browser'), + message_limit: createViolationInstance('message_limit'), + token_balance: createViolationInstance('token_balance'), + registrations: createViolationInstance('registrations'), + logins: createViolationInstance('logins'), }; /** - * Returns either the logs of violations specified by type if a type is provided - * or it returns the general log if no type is specified. If an invalid type is passed, - * an error will be thrown. + * Returns the keyv cache specified by type. + * If an invalid type is passed, an error will be thrown. * * @module getLogStores * @requires keyv - a simple key-value storage that allows you to easily switch out storage adapters. @@ -31,11 +42,10 @@ const namespaces = { * @throws Will throw an error if an invalid violation type is passed. */ const getLogStores = (type) => { - if (!type) { + if (!type || !namespaces[type]) { throw new Error(`Invalid store type: ${type}`); } - const logs = namespaces[type]; - return logs; + return namespaces[type]; }; module.exports = getLogStores; diff --git a/api/cache/index.js b/api/cache/index.js index 1edbf981d9..bb1e774183 100644 --- a/api/cache/index.js +++ b/api/cache/index.js @@ -1,6 +1,5 @@ const keyvFiles = require('./keyvFiles'); const getLogStores = require('./getLogStores'); const logViolation = require('./logViolation'); -const clearPendingReq = require('./clearPendingReq'); -module.exports = { ...keyvFiles, getLogStores, logViolation, clearPendingReq }; +module.exports = { ...keyvFiles, getLogStores, logViolation }; diff --git a/api/cache/keyvRedis.js b/api/cache/keyvRedis.js new file mode 100644 index 0000000000..942b1b239f --- /dev/null +++ b/api/cache/keyvRedis.js @@ -0,0 +1,14 @@ +const KeyvRedis = require('@keyv/redis'); + +const { REDIS_URI } = process.env; + +let keyvRedis; + +if (REDIS_URI) { + keyvRedis = new KeyvRedis(REDIS_URI, { useRedisSets: false }); + keyvRedis.on('error', (err) => console.error('KeyvRedis connection error:', err)); +} else { + // console.log('REDIS_URI not provided. Redis module will not be initialized.'); +} + +module.exports = keyvRedis; diff --git a/api/cache/logViolation.js b/api/cache/logViolation.js index 9f045421a9..7fe85afd8a 100644 --- a/api/cache/logViolation.js +++ b/api/cache/logViolation.js @@ -1,5 +1,6 @@ const getLogStores = require('./getLogStores'); const banViolation = require('./banViolation'); +const { isEnabled } = require('../server/utils'); /** * Logs the violation. @@ -17,10 +18,11 @@ const logViolation = async (req, res, type, errorMessage, score = 1) => { } const logs = getLogStores('general'); const violationLogs = getLogStores(type); + const key = isEnabled(process.env.USE_REDIS) ? `${type}:${userId}` : userId; - const userViolations = (await violationLogs.get(userId)) ?? 0; - const violationCount = userViolations + score; - await violationLogs.set(userId, violationCount); + const userViolations = (await violationLogs.get(key)) ?? 0; + const violationCount = +userViolations + +score; + await violationLogs.set(key, violationCount); errorMessage.user_id = userId; errorMessage.prev_count = userViolations; @@ -28,10 +30,10 @@ const logViolation = async (req, res, type, errorMessage, score = 1) => { errorMessage.date = new Date().toISOString(); await banViolation(req, res, errorMessage); - const userLogs = (await logs.get(userId)) ?? []; + const userLogs = (await logs.get(key)) ?? []; userLogs.push(errorMessage); delete errorMessage.user_id; - await logs.set(userId, userLogs); + await logs.set(key, userLogs); }; module.exports = logViolation; diff --git a/api/cache/redis.js b/api/cache/redis.js new file mode 100644 index 0000000000..adf291d02b --- /dev/null +++ b/api/cache/redis.js @@ -0,0 +1,4 @@ +const Redis = require('ioredis'); +const { REDIS_URI } = process.env ?? {}; +const redis = new Redis.Cluster(REDIS_URI); +module.exports = redis; diff --git a/api/jsconfig.json b/api/jsconfig.json new file mode 100644 index 0000000000..756746fbf8 --- /dev/null +++ b/api/jsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "target": "ES6", + "module": "CommonJS", + // "checkJs": true, // Report errors in JavaScript files + "baseUrl": "./", + "paths": { + "*": ["*", "node_modules/*"], + "~/*": ["./*"] + } + }, + "exclude": ["node_modules"] +} diff --git a/api/lib/db/connectDb.js b/api/lib/db/connectDb.js index 8b9cdae012..3e711ca7ad 100644 --- a/api/lib/db/connectDb.js +++ b/api/lib/db/connectDb.js @@ -18,11 +18,12 @@ if (!cached) { } async function connectDb() { - if (cached.conn) { + if (cached.conn && cached.conn?._readyState === 1) { return cached.conn; } - if (!cached.promise) { + const disconnected = cached.conn && cached.conn?._readyState !== 1; + if (!cached.promise || disconnected) { const opts = { useNewUrlParser: true, useUnifiedTopology: true, diff --git a/api/package.json b/api/package.json index f01b8c55c9..4453c9c269 100644 --- a/api/package.json +++ b/api/package.json @@ -24,11 +24,13 @@ "@anthropic-ai/sdk": "^0.5.4", "@azure/search-documents": "^11.3.2", "@keyv/mongo": "^2.1.8", + "@keyv/redis": "^2.8.0", "@waylaidwanderer/chatgpt-api": "^1.37.2", "axios": "^1.3.4", "bcryptjs": "^2.4.3", "cheerio": "^1.0.0-rc.12", "cohere-ai": "^6.0.0", + "connect-redis": "^7.1.0", "cookie": "^0.5.0", "cors": "^2.8.5", "dotenv": "^16.0.3", @@ -39,10 +41,11 @@ "googleapis": "^118.0.0", "handlebars": "^4.7.7", "html": "^1.0.0", + "ioredis": "^5.3.2", "jose": "^4.15.2", "js-yaml": "^4.1.0", "jsonwebtoken": "^9.0.0", - "keyv": "^4.5.3", + "keyv": "^4.5.4", "keyv-file": "^0.2.0", "langchain": "^0.0.153", "lodash": "^4.17.21", diff --git a/api/server/middleware/abortMiddleware.js b/api/server/middleware/abortMiddleware.js index fc9a441556..a65d09c884 100644 --- a/api/server/middleware/abortMiddleware.js +++ b/api/server/middleware/abortMiddleware.js @@ -1,5 +1,6 @@ +const { sendMessage, sendError, countTokens, isEnabled } = require('../utils'); const { saveMessage, getConvo, getConvoTitle } = require('../../models'); -const { sendMessage, sendError, countTokens } = require('../utils'); +const clearPendingReq = require('../../cache/clearPendingReq'); const spendTokens = require('../../models/spendTokens'); const abortControllers = require('./abortControllers'); @@ -20,6 +21,9 @@ async function abortMessage(req, res) { const handleAbort = () => { return async (req, res) => { try { + if (isEnabled(process.env.LIMIT_CONCURRENT_MESSAGES)) { + await clearPendingReq({ userId: req.user.id }); + } return await abortMessage(req, res); } catch (err) { console.error(err); diff --git a/api/server/middleware/checkBan.js b/api/server/middleware/checkBan.js index 294f4a668b..c744dda07d 100644 --- a/api/server/middleware/checkBan.js +++ b/api/server/middleware/checkBan.js @@ -3,8 +3,11 @@ const uap = require('ua-parser-js'); const { getLogStores } = require('../../cache'); const denyRequest = require('./denyRequest'); const { isEnabled, removePorts } = require('../utils'); +const keyvRedis = require('../../cache/keyvRedis'); -const banCache = new Keyv({ namespace: 'bans', ttl: 0 }); +const banCache = isEnabled(process.env.USE_REDIS) + ? new Keyv({ store: keyvRedis }) + : new Keyv({ namespace: 'bans', ttl: 0 }); const message = 'Your account has been temporarily banned due to violations of our service.'; /** @@ -50,9 +53,11 @@ const checkBan = async (req, res, next = () => {}) => { req.ip = removePorts(req); const userId = req.user?.id ?? req.user?._id ?? null; + const ipKey = isEnabled(process.env.USE_REDIS) ? `ban_cache:ip:${req.ip}` : req.ip; + const userKey = isEnabled(process.env.USE_REDIS) ? `ban_cache:user:${userId}` : userId; - const cachedIPBan = await banCache.get(req.ip); - const cachedUserBan = await banCache.get(userId); + const cachedIPBan = await banCache.get(ipKey); + const cachedUserBan = await banCache.get(userKey); const cachedBan = cachedIPBan || cachedUserBan; if (cachedBan) { @@ -78,13 +83,13 @@ const checkBan = async (req, res, next = () => {}) => { const timeLeft = Number(isBanned.expiresAt) - Date.now(); if (timeLeft <= 0) { - await banLogs.delete(req.ip); - await banLogs.delete(userId); + await banLogs.delete(ipKey); + await banLogs.delete(userKey); return next(); } - banCache.set(req.ip, isBanned, timeLeft); - banCache.set(userId, isBanned, timeLeft); + banCache.set(ipKey, isBanned, timeLeft); + banCache.set(userKey, isBanned, timeLeft); req.banned = true; return await banResponse(req, res); }; diff --git a/api/server/middleware/concurrentLimiter.js b/api/server/middleware/concurrentLimiter.js index d110b1b86f..402152eb02 100644 --- a/api/server/middleware/concurrentLimiter.js +++ b/api/server/middleware/concurrentLimiter.js @@ -1,10 +1,13 @@ -const Keyv = require('keyv'); -const { logViolation } = require('../../cache'); - +const clearPendingReq = require('../../cache/clearPendingReq'); +const { logViolation, getLogStores } = require('../../cache'); const denyRequest = require('./denyRequest'); -// Serve cache from memory so no need to clear it on startup/exit -const pendingReqCache = new Keyv({ namespace: 'pendingRequests' }); +const { + USE_REDIS, + CONCURRENT_MESSAGE_MAX = 1, + CONCURRENT_VIOLATION_SCORE: score, +} = process.env ?? {}; +const ttl = 1000 * 60 * 1; /** * Middleware to limit concurrent requests for a user. @@ -12,7 +15,7 @@ const pendingReqCache = new Keyv({ namespace: 'pendingRequests' }); * This middleware checks if a user has exceeded a specified concurrent request limit. * If the user exceeds the limit, an error is returned. If the user is within the limit, * their request count is incremented. After the request is processed, the count is decremented. - * If the `pendingReqCache` store is not available, the middleware will skip its logic. + * If the `cache` store is not available, the middleware will skip its logic. * * @function * @param {Object} req - Express request object containing user information. @@ -21,7 +24,9 @@ const pendingReqCache = new Keyv({ namespace: 'pendingRequests' }); * @throws {Error} Throws an error if the user exceeds the concurrent request limit. */ const concurrentLimiter = async (req, res, next) => { - if (!pendingReqCache) { + const namespace = 'pending_req'; + const cache = getLogStores(namespace); + if (!cache) { return next(); } @@ -29,12 +34,12 @@ const concurrentLimiter = async (req, res, next) => { return next(); } - const { CONCURRENT_MESSAGE_MAX = 1, CONCURRENT_VIOLATION_SCORE: score } = process.env; + const userId = req.user?.id ?? req.user?._id ?? ''; const limit = Math.max(CONCURRENT_MESSAGE_MAX, 1); const type = 'concurrent'; - const userId = req.user?.id ?? req.user?._id ?? null; - const pendingRequests = (await pendingReqCache.get(userId)) ?? 0; + const key = `${USE_REDIS ? namespace : ''}:${userId}`; + const pendingRequests = +((await cache.get(key)) ?? 0); if (pendingRequests >= limit) { const errorMessage = { @@ -46,22 +51,17 @@ const concurrentLimiter = async (req, res, next) => { await logViolation(req, res, type, errorMessage, score); return await denyRequest(req, res, errorMessage); } else { - await pendingReqCache.set(userId, pendingRequests + 1); + await cache.set(key, pendingRequests + 1, ttl); } // Ensure the requests are removed from the store once the request is done + let cleared = false; const cleanUp = async () => { - if (!pendingReqCache) { + if (cleared) { return; } - - const currentRequests = await pendingReqCache.get(userId); - - if (currentRequests && currentRequests >= 1) { - await pendingReqCache.set(userId, currentRequests - 1); - } else { - await pendingReqCache.delete(userId); - } + cleared = true; + await clearPendingReq({ userId, cache }); }; if (pendingRequests < limit) { @@ -72,10 +72,4 @@ const concurrentLimiter = async (req, res, next) => { next(); }; -// if cache is not served from memory, clear it on exit -// process.on('exit', async () => { -// console.log('Clearing all pending requests before exiting...'); -// await pendingReqCache.clear(); -// }); - module.exports = concurrentLimiter; diff --git a/api/server/routes/search.js b/api/server/routes/search.js index 955e58da97..98720a2ae5 100644 --- a/api/server/routes/search.js +++ b/api/server/routes/search.js @@ -1,3 +1,4 @@ +const Keyv = require('keyv'); const express = require('express'); const router = express.Router(); const { MeiliSearch } = require('meilisearch'); @@ -6,8 +7,15 @@ const { Conversation, getConvosQueried } = require('../../models/Conversation'); const { reduceHits } = require('../../lib/utils/reduceHits'); const { cleanUpPrimaryKeyValue } = require('../../lib/utils/misc'); const requireJwtAuth = require('../middleware/requireJwtAuth'); +const keyvRedis = require('../../cache/keyvRedis'); +const { isEnabled } = require('../utils'); -const cache = new Map(); +const expiration = 60 * 1000; +const cache = isEnabled(process.env.USE_REDIS) + ? new Keyv({ store: keyvRedis }) + : new Keyv({ namespace: 'search', ttl: expiration }); + +router.use(requireJwtAuth); router.get('/sync', async function (req, res) { await Message.syncWithMeili(); @@ -15,24 +23,20 @@ router.get('/sync', async function (req, res) { res.send('synced'); }); -router.get('/', requireJwtAuth, async function (req, res) { +router.get('/', async function (req, res) { try { - let user = req.user.id; - user = user ?? null; + let user = req.user.id ?? ''; const { q } = req.query; const pageNumber = req.query.pageNumber || 1; - const key = `${user || ''}${q}`; - - if (cache.has(key)) { + const key = `${user}:search:${q}`; + const cached = await cache.get(key); + if (cached) { console.log('cache hit', key); - const cached = cache.get(key); const { pages, pageSize, messages } = cached; res .status(200) .send({ conversations: cached[pageNumber], pages, pageNumber, pageSize, messages }); return; - } else { - cache.clear(); } // const message = await Message.meiliSearch(q); @@ -67,7 +71,7 @@ router.get('/', requireJwtAuth, async function (req, res) { if (message.conversationId.includes('--')) { message.conversationId = cleanUpPrimaryKeyValue(message.conversationId); } - if (result.convoMap[message.conversationId] && !message.error) { + if (result.convoMap[message.conversationId]) { const convo = result.convoMap[message.conversationId]; const { title, chatGptLabel, model } = convo; message = { ...message, ...{ title, chatGptLabel, model } }; @@ -77,7 +81,7 @@ router.get('/', requireJwtAuth, async function (req, res) { result.messages = activeMessages; if (result.cache) { result.cache.messages = activeMessages; - cache.set(key, result.cache); + cache.set(key, result.cache, expiration); delete result.cache; } delete result.convoMap; diff --git a/api/server/services/ModelService.js b/api/server/services/ModelService.js index 41d4290ffb..7789e70cf2 100644 --- a/api/server/services/ModelService.js +++ b/api/server/services/ModelService.js @@ -1,9 +1,13 @@ const Keyv = require('keyv'); const axios = require('axios'); +const { isEnabled } = require('../utils'); +const keyvRedis = require('../../cache/keyvRedis'); // const { getAzureCredentials, genAzureChatCompletion } = require('../../utils/'); const { openAIApiKey, userProvidedOpenAI } = require('./EndpointService').config; -const modelsCache = new Keyv({ namespace: 'models' }); +const modelsCache = isEnabled(process.env.USE_REDIS) + ? new Keyv({ store: keyvRedis }) + : new Keyv({ namespace: 'models' }); const { OPENROUTER_API_KEY, OPENAI_REVERSE_PROXY, CHATGPT_MODELS, ANTHROPIC_MODELS } = process.env ?? {}; diff --git a/api/server/socialLogins.js b/api/server/socialLogins.js index ece87a2edc..af61db73e9 100644 --- a/api/server/socialLogins.js +++ b/api/server/socialLogins.js @@ -1,4 +1,5 @@ const session = require('express-session'); +const RedisStore = require('connect-redis').default; const passport = require('passport'); const { googleLogin, @@ -7,6 +8,7 @@ const { facebookLogin, setupOpenId, } = require('../strategies'); +const client = require('../cache/redis'); const configureSocialLogins = (app) => { if (process.env.GOOGLE_CLIENT_ID && process.env.GOOGLE_CLIENT_SECRET) { @@ -28,13 +30,15 @@ const configureSocialLogins = (app) => { process.env.OPENID_SCOPE && process.env.OPENID_SESSION_SECRET ) { - app.use( - session({ - secret: process.env.OPENID_SESSION_SECRET, - resave: false, - saveUninitialized: false, - }), - ); + const sessionOptions = { + secret: process.env.OPENID_SESSION_SECRET, + resave: false, + saveUninitialized: false, + }; + if (process.env.USE_REDIS) { + sessionOptions.store = new RedisStore({ client, prefix: 'librechat' }); + } + app.use(session(sessionOptions)); app.use(passport.session()); setupOpenId(); } diff --git a/bun.lockb b/bun.lockb index 1e71e31ce8..f1262564fe 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/client/src/components/Messages/Content/Error.tsx b/client/src/components/Messages/Content/Error.tsx index 5d19ef2956..ed6f4b280d 100644 --- a/client/src/components/Messages/Content/Error.tsx +++ b/client/src/components/Messages/Content/Error.tsx @@ -7,7 +7,6 @@ const isJson = (str: string) => { try { JSON.parse(str); } catch (e) { - console.error(e); return false; } return true; diff --git a/client/src/components/Messages/Message.tsx b/client/src/components/Messages/Message.tsx index ef857f98a3..8f6f50e355 100644 --- a/client/src/components/Messages/Message.tsx +++ b/client/src/components/Messages/Message.tsx @@ -170,7 +170,7 @@ export default function Message({ text={text ?? ''} message={message} enterEdit={enterEdit} - error={error ?? false} + error={!!(error && !searchResult)} isSubmitting={isSubmitting} unfinished={unfinished ?? false} isCreatedByUser={isCreatedByUser ?? true} diff --git a/client/src/components/Nav/Nav.tsx b/client/src/components/Nav/Nav.tsx index 4bf54b583f..fae97366fe 100644 --- a/client/src/components/Nav/Nav.tsx +++ b/client/src/components/Nav/Nav.tsx @@ -13,7 +13,6 @@ import { Panel, Spinner } from '~/components'; import { Conversations, Pages } from '../Conversations'; import { useAuthContext, - useDebounce, useMediaQuery, useLocalize, useConversation, @@ -67,14 +66,8 @@ export default function Nav({ navVisible, setNavVisible }) { const [isFetching, setIsFetching] = useState(false); - const debouncedSearchTerm = useDebounce(searchQuery, 750); - const searchQueryFn = useSearchQuery(debouncedSearchTerm, pageNumber + '', { - enabled: !!( - !!debouncedSearchTerm && - debouncedSearchTerm.length > 0 && - isSearchEnabled && - isSearching - ), + const searchQueryFn = useSearchQuery(searchQuery, pageNumber + '', { + enabled: !!(!!searchQuery && searchQuery.length > 0 && isSearchEnabled && isSearching), }); const onSearchSuccess = useCallback((data: TSearchResults, expectedPage?: number) => { diff --git a/client/src/components/Nav/NavLinks.tsx b/client/src/components/Nav/NavLinks.tsx index e04b6ed27c..8ca9febb02 100644 --- a/client/src/components/Nav/NavLinks.tsx +++ b/client/src/components/Nav/NavLinks.tsx @@ -55,6 +55,7 @@ export default function NavLinks() { 'group-ui-open:bg-gray-800 flex w-full items-center gap-2.5 rounded-md px-3 py-3 text-sm transition-colors duration-200 hover:bg-gray-800', open ? 'bg-gray-800' : '', )} + data-testid="nav-user" >
diff --git a/client/src/components/Nav/SearchBar.tsx b/client/src/components/Nav/SearchBar.tsx index 142daafa54..6f6fe6c21d 100644 --- a/client/src/components/Nav/SearchBar.tsx +++ b/client/src/components/Nav/SearchBar.tsx @@ -1,6 +1,7 @@ -import { forwardRef, useState, useEffect, Ref } from 'react'; +import { forwardRef, useState, useCallback, useMemo, Ref } from 'react'; import { Search, X } from 'lucide-react'; -import { useRecoilState } from 'recoil'; +import { useSetRecoilState } from 'recoil'; +import debounce from 'lodash/debounce'; import { useLocalize } from '~/hooks'; import store from '~/store'; @@ -10,33 +11,35 @@ type SearchBarProps = { const SearchBar = forwardRef((props: SearchBarProps, ref: Ref) => { const { clearSearch } = props; - const [searchQuery, setSearchQuery] = useRecoilState(store.searchQuery); + const setSearchQuery = useSetRecoilState(store.searchQuery); const [showClearIcon, setShowClearIcon] = useState(false); + const [text, setText] = useState(''); const localize = useLocalize(); + const clearText = useCallback(() => { + setShowClearIcon(false); + setSearchQuery(''); + clearSearch(); + setText(''); + }, [setSearchQuery, clearSearch]); + const handleKeyUp = (e: React.KeyboardEvent) => { const { value } = e.target as HTMLInputElement; - /* TODO: deprecated keyCode */ - if (e.keyCode === 8 && value === '') { - setSearchQuery(''); - clearSearch(); + if (e.key === 'Backspace' && value === '') { + clearText(); } }; + const sendRequest = useCallback((value: string) => setSearchQuery(value), [setSearchQuery]); + const debouncedSendRequest = useMemo(() => debounce(sendRequest, 350), [sendRequest]); + const onChange = (e: React.FormEvent) => { const { value } = e.target as HTMLInputElement; - setSearchQuery(value); setShowClearIcon(value.length > 0); + setText(value); + debouncedSendRequest(value); }; - useEffect(() => { - if (searchQuery.length === 0) { - setShowClearIcon(false); - } else { - setShowClearIcon(true); - } - }, [searchQuery]); - return (
) = { e.code === 'Space' ? e.stopPropagation() : null; @@ -58,10 +61,7 @@ const SearchBar = forwardRef((props: SearchBarProps, ref: Ref) = className={`absolute right-3 h-5 w-5 cursor-pointer ${ showClearIcon ? 'opacity-100' : 'opacity-0' } transition-opacity duration-1000`} - onClick={() => { - setSearchQuery(''); - clearSearch(); - }} + onClick={clearText} />
); diff --git a/client/src/routes/Root.tsx b/client/src/routes/Root.tsx index 4da530b2d4..c4711fb02e 100644 --- a/client/src/routes/Root.tsx +++ b/client/src/routes/Root.tsx @@ -29,8 +29,8 @@ export default function Root() { const setEndpointsConfig = useSetRecoilState(store.endpointsConfig); const setModelsConfig = useSetRecoilState(store.modelsConfig); - const searchEnabledQuery = useGetSearchEnabledQuery(); const endpointsQuery = useGetEndpointsQuery(); + const searchEnabledQuery = useGetSearchEnabledQuery({ enabled: isAuthenticated }); const modelsQuery = useGetModelsQuery({ enabled: isAuthenticated }); const presetsQuery = useGetPresetsQuery({ enabled: !!user }); diff --git a/client/src/store/endpoints.ts b/client/src/store/endpoints.ts index cbcd3e54fc..458c40ad31 100644 --- a/client/src/store/endpoints.ts +++ b/client/src/store/endpoints.ts @@ -1,17 +1,19 @@ import { atom, selector } from 'recoil'; import { TEndpointsConfig } from 'librechat-data-provider'; +const defaultConfig: TEndpointsConfig = { + azureOpenAI: null, + openAI: null, + bingAI: null, + chatGPTBrowser: null, + gptPlugins: null, + google: null, + anthropic: null, +}; + const endpointsConfig = atom({ key: 'endpointsConfig', - default: { - azureOpenAI: null, - openAI: null, - bingAI: null, - chatGPTBrowser: null, - gptPlugins: null, - google: null, - anthropic: null, - }, + default: defaultConfig, }); const plugins = selector({ @@ -58,4 +60,5 @@ export default { endpointsConfig, endpointsFilter, availableEndpoints, + defaultConfig, }; diff --git a/client/src/utils/getDefaultEndpoint.ts b/client/src/utils/getDefaultEndpoint.ts index 1b960996e5..4f1e9f74b6 100644 --- a/client/src/utils/getDefaultEndpoint.ts +++ b/client/src/utils/getDefaultEndpoint.ts @@ -28,11 +28,18 @@ const getEndpointFromSetup = (convoSetup: TConvoSetup, endpointsConfig: TEndpoin const getEndpointFromLocalStorage = (endpointsConfig: TEndpointsConfig) => { try { const { lastConversationSetup } = getLocalStorageItems(); + const { endpoint } = lastConversationSetup; + const isDefaultConfig = Object.values(endpointsConfig ?? {})?.every((value) => !value); - return ( - lastConversationSetup.endpoint && - (endpointsConfig[lastConversationSetup.endpoint] ? lastConversationSetup.endpoint : null) - ); + if (isDefaultConfig && endpoint) { + return endpoint; + } + + if (isDefaultConfig && endpoint) { + return endpoint; + } + + return endpoint && endpointsConfig[endpoint] ? endpoint : null; } catch (error) { console.error(error); return null; diff --git a/e2e/setup/authenticate.ts b/e2e/setup/authenticate.ts index 9e91efd9f2..6abac7a7e4 100644 --- a/e2e/setup/authenticate.ts +++ b/e2e/setup/authenticate.ts @@ -1,8 +1,10 @@ import { Page, FullConfig, chromium } from '@playwright/test'; +import cleanupUser from './cleanupUser'; import dotenv from 'dotenv'; dotenv.config(); type User = { email: string; name: string; password: string }; +const timeout = 3500; async function register(page: Page, user: User) { await page.getByRole('link', { name: 'Sign up' }).click(); @@ -52,18 +54,31 @@ async function authenticate(config: FullConfig, user: User) { }); console.log('🤖: ✔️ localStorage: set Nav as Visible', storageState); - await page.goto(baseURL, { timeout: 5000 }); + await page.goto(baseURL, { timeout }); await register(page, user); - await page.waitForURL(`${baseURL}/chat/new`); + try { + await page.waitForURL(`${baseURL}/chat/new`, { timeout }); + } catch (error) { + console.error('Error:', error); + const userExists = page.getByTestId('registration-error'); + if (userExists) { + console.log('🤖: 🚨 user already exists'); + await cleanupUser(user); + await page.goto(baseURL, { timeout }); + await register(page, user); + } else { + throw new Error('🤖: 🚨 user failed to register'); + } + } console.log('🤖: ✔️ user successfully registered'); // Logout await logout(page, user); - await page.waitForURL(`${baseURL}/login`); + await page.waitForURL(`${baseURL}/login`, { timeout }); console.log('🤖: ✔️ user successfully logged out'); await login(page, user); - await page.waitForURL(`${baseURL}/chat/new`); + await page.waitForURL(`${baseURL}/chat/new`, { timeout }); console.log('🤖: ✔️ user successfully authenticated'); await page.context().storageState({ path: storageState as string }); diff --git a/e2e/specs/messages.spec.ts b/e2e/specs/messages.spec.ts index 76cf0a5409..f9593c06c2 100644 --- a/e2e/specs/messages.spec.ts +++ b/e2e/specs/messages.spec.ts @@ -38,14 +38,6 @@ test.beforeAll(async ({ browser }) => { await page.close(); }); -test.afterAll(async () => { - console.log('🤖: clearing conversations after message tests.'); - const page = await beforeAfterAllContext.newPage(); - await clearConvos(page); - await page.close(); - await beforeAfterAllContext.close(); -}); - test.beforeEach(async ({ page }) => { await page.goto(initialUrl, { timeout: 5000 }); }); diff --git a/e2e/specs/nav.spec.ts b/e2e/specs/nav.spec.ts index d8f9970582..ff1f5e16b3 100644 --- a/e2e/specs/nav.spec.ts +++ b/e2e/specs/nav.spec.ts @@ -4,14 +4,14 @@ test.describe('Navigation suite', () => { test('Navigation bar', async ({ page }) => { await page.goto('http://localhost:3080/', { timeout: 5000 }); - await page.locator('[id="headlessui-menu-button-\\:r0\\:"]').click(); - const navBar = await page.locator('[id="headlessui-menu-button-\\:r0\\:"]').isVisible(); - expect(navBar).toBeTruthy(); + await page.getByTestId('nav-user').click(); + const navSettings = await page.getByTestId('nav-user').isVisible(); + expect(navSettings).toBeTruthy(); }); test('Settings modal', async ({ page }) => { await page.goto('http://localhost:3080/', { timeout: 5000 }); - await page.locator('[id="headlessui-menu-button-\\:r0\\:"]').click(); + await page.getByTestId('nav-user').click(); await page.getByText('Settings').click(); const modal = await page.getByRole('dialog', { name: 'Settings' }).isVisible(); diff --git a/package-lock.json b/package-lock.json index c4ca442c93..a676c063e6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,7 +15,7 @@ "packages/*" ], "devDependencies": { - "@playwright/test": "^1.32.1", + "@playwright/test": "^1.38.1", "@typescript-eslint/eslint-plugin": "^5.62.0", "@typescript-eslint/parser": "^5.62.0", "cross-env": "^7.0.3", @@ -45,11 +45,13 @@ "@anthropic-ai/sdk": "^0.5.4", "@azure/search-documents": "^11.3.2", "@keyv/mongo": "^2.1.8", + "@keyv/redis": "^2.8.0", "@waylaidwanderer/chatgpt-api": "^1.37.2", "axios": "^1.3.4", "bcryptjs": "^2.4.3", "cheerio": "^1.0.0-rc.12", "cohere-ai": "^6.0.0", + "connect-redis": "^7.1.0", "cookie": "^0.5.0", "cors": "^2.8.5", "dotenv": "^16.0.3", @@ -60,10 +62,11 @@ "googleapis": "^118.0.0", "handlebars": "^4.7.7", "html": "^1.0.0", + "ioredis": "^5.3.2", "jose": "^4.15.2", "js-yaml": "^4.1.0", "jsonwebtoken": "^9.0.0", - "keyv": "^4.5.3", + "keyv": "^4.5.4", "keyv-file": "^0.2.0", "langchain": "^0.0.153", "lodash": "^4.17.21", @@ -4850,6 +4853,11 @@ "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", "dev": true }, + "node_modules/@ioredis/commands": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.2.0.tgz", + "integrity": "sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg==" + }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", @@ -5419,6 +5427,17 @@ "@mongodb-js/saslprep": "^1.1.0" } }, + "node_modules/@keyv/redis": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@keyv/redis/-/redis-2.8.0.tgz", + "integrity": "sha512-6k7wG/KKSIGpruKlsEB4sFjECJEyQsuJbWoWdoq9Uv2L6Mm/SEqEidekRZI/QljE1A4WQkFsIE8hHl1Oc3UNGg==", + "dependencies": { + "ioredis": "^5.3.2" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/@librechat/backend": { "resolved": "api", "link": true @@ -5531,12 +5550,12 @@ } }, "node_modules/@playwright/test": { - "version": "1.38.0", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.38.0.tgz", - "integrity": "sha512-xis/RXXsLxwThKnlIXouxmIvvT3zvQj1JE39GsNieMUrMpb3/GySHDh2j8itCG22qKVD4MYLBp7xB73cUW/UUw==", + "version": "1.38.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.38.1.tgz", + "integrity": "sha512-NqRp8XMwj3AK+zKLbZShl0r/9wKgzqI/527bkptKXomtuo+dOjU9NdMASQ8DNC9z9zLOMbG53T4eihYr3XR+BQ==", "dev": true, "dependencies": { - "playwright": "1.38.0" + "playwright": "1.38.1" }, "bin": { "playwright": "cli.js" @@ -6500,6 +6519,78 @@ "node": ">=8.x" } }, + "node_modules/@redis/bloom": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@redis/bloom/-/bloom-1.2.0.tgz", + "integrity": "sha512-HG2DFjYKbpNmVXsa0keLHp/3leGJz1mjh09f2RLGGLQZzSHpkmZWuwJbAvo3QcRY8p80m5+ZdXZdYOSBLlp7Cg==", + "optional": true, + "peer": true, + "peerDependencies": { + "@redis/client": "^1.0.0" + } + }, + "node_modules/@redis/client": { + "version": "1.5.11", + "resolved": "https://registry.npmjs.org/@redis/client/-/client-1.5.11.tgz", + "integrity": "sha512-cV7yHcOAtNQ5x/yQl7Yw1xf53kO0FNDTdDU6bFIMbW6ljB7U7ns0YRM+QIkpoqTAt6zK5k9Fq0QWlUbLcq9AvA==", + "optional": true, + "peer": true, + "dependencies": { + "cluster-key-slot": "1.1.2", + "generic-pool": "3.9.0", + "yallist": "4.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@redis/client/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "optional": true, + "peer": true + }, + "node_modules/@redis/graph": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@redis/graph/-/graph-1.1.0.tgz", + "integrity": "sha512-16yZWngxyXPd+MJxeSr0dqh2AIOi8j9yXKcKCwVaKDbH3HTuETpDVPcLujhFYVPtYrngSco31BUcSa9TH31Gqg==", + "optional": true, + "peer": true, + "peerDependencies": { + "@redis/client": "^1.0.0" + } + }, + "node_modules/@redis/json": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@redis/json/-/json-1.0.6.tgz", + "integrity": "sha512-rcZO3bfQbm2zPRpqo82XbW8zg4G/w4W3tI7X8Mqleq9goQjAGLL7q/1n1ZX4dXEAmORVZ4s1+uKLaUOg7LrUhw==", + "optional": true, + "peer": true, + "peerDependencies": { + "@redis/client": "^1.0.0" + } + }, + "node_modules/@redis/search": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@redis/search/-/search-1.1.5.tgz", + "integrity": "sha512-hPP8w7GfGsbtYEJdn4n7nXa6xt6hVZnnDktKW4ArMaFQ/m/aR7eFvsLQmG/mn1Upq99btPJk+F27IQ2dYpCoUg==", + "optional": true, + "peer": true, + "peerDependencies": { + "@redis/client": "^1.0.0" + } + }, + "node_modules/@redis/time-series": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@redis/time-series/-/time-series-1.0.5.tgz", + "integrity": "sha512-IFjIgTusQym2B5IZJG3XKr5llka7ey84fw/NOYqESP5WUfQs9zz1ww/9+qoz4ka/S6KcGBodzlCeZ5UImKbscg==", + "optional": true, + "peer": true, + "peerDependencies": { + "@redis/client": "^1.0.0" + } + }, "node_modules/@remix-run/router": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.9.0.tgz", @@ -9682,6 +9773,14 @@ "node": ">=6" } }, + "node_modules/cluster-key-slot": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz", + "integrity": "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/co": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", @@ -9856,6 +9955,17 @@ "node": ">=0.8" } }, + "node_modules/connect-redis": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/connect-redis/-/connect-redis-7.1.0.tgz", + "integrity": "sha512-UaqO1EirWjON2ENsyau7N5lbkrdYBpS6mYlXSeff/OYXsd6EGZ+SXSmNPoljL2PSua8fgjAEaldSA73PMZQ9Eg==", + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "express-session": ">=1" + } + }, "node_modules/consola": { "version": "2.15.3", "resolved": "https://registry.npmjs.org/consola/-/consola-2.15.3.tgz", @@ -10399,6 +10509,14 @@ "node": ">=0.4.0" } }, + "node_modules/denque": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", + "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", + "engines": { + "node": ">=0.10" + } + }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -12368,6 +12486,16 @@ "node": ">=12" } }, + "node_modules/generic-pool": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/generic-pool/-/generic-pool-3.9.0.tgz", + "integrity": "sha512-hymDOu5B53XvN4QT9dBmZxPX4CWhBPPLguTZ9MMFeFa/Kg0xWVfylOVNlJji/E7yTZWFd/q9GO5TxDLq156D7g==", + "optional": true, + "peer": true, + "engines": { + "node": ">= 4" + } + }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -13636,6 +13764,29 @@ "loose-envify": "^1.0.0" } }, + "node_modules/ioredis": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.3.2.tgz", + "integrity": "sha512-1DKMMzlIHM02eBBVOFQ1+AolGjs6+xEcM4PDL7NqOS6szq7H9jSaEkIUH6/a5Hl241LzW6JLSiAbNvTQjUupUA==", + "dependencies": { + "@ioredis/commands": "^1.1.1", + "cluster-key-slot": "^1.1.0", + "debug": "^4.3.4", + "denque": "^2.1.0", + "lodash.defaults": "^4.2.0", + "lodash.isarguments": "^3.1.0", + "redis-errors": "^1.2.0", + "redis-parser": "^3.0.0", + "standard-as-callback": "^2.1.0" + }, + "engines": { + "node": ">=12.22.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/ioredis" + } + }, "node_modules/ip": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ip/-/ip-2.0.0.tgz", @@ -15222,9 +15373,9 @@ } }, "node_modules/keyv": { - "version": "4.5.3", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.3.tgz", - "integrity": "sha512-QCiSav9WaX1PgETJ+SpNnx2PRRapJ/oRSXM4VO5OGYGSjrxbKPVFVhB3l2OCbLCk329N8qyAtsJjSjvVBWzEug==", + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", "dependencies": { "json-buffer": "3.0.1" } @@ -15583,11 +15734,21 @@ "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", "dev": true }, + "node_modules/lodash.defaults": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", + "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==" + }, "node_modules/lodash.includes": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==" }, + "node_modules/lodash.isarguments": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", + "integrity": "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==" + }, "node_modules/lodash.isboolean": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", @@ -18422,12 +18583,12 @@ } }, "node_modules/playwright": { - "version": "1.38.0", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.38.0.tgz", - "integrity": "sha512-fJGw+HO0YY+fU/F1N57DMO+TmXHTrmr905J05zwAQE9xkuwP/QLDk63rVhmyxh03dYnEhnRbsdbH9B0UVVRB3A==", + "version": "1.38.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.38.1.tgz", + "integrity": "sha512-oRMSJmZrOu1FP5iu3UrCx8JEFRIMxLDM0c/3o4bpzU5Tz97BypefWf7TuTNPWeCe279TPal5RtPPZ+9lW/Qkow==", "devOptional": true, "dependencies": { - "playwright-core": "1.38.0" + "playwright-core": "1.38.1" }, "bin": { "playwright": "cli.js" @@ -18440,9 +18601,9 @@ } }, "node_modules/playwright-core": { - "version": "1.38.0", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.38.0.tgz", - "integrity": "sha512-f8z1y8J9zvmHoEhKgspmCvOExF2XdcxMW8jNRuX4vkQFrzV4MlZ55iwb5QeyiFQgOFCUolXiRHgpjSEnqvO48g==", + "version": "1.38.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.38.1.tgz", + "integrity": "sha512-tQqNFUKa3OfMf4b2jQ7aGLB8o9bS3bOY0yMEtldtC2+spf8QXG9zvXLTXUeRsoNuxEYMgLYR+NXfAa1rjKRcrg==", "devOptional": true, "bin": { "playwright-core": "cli.js" @@ -20348,6 +20509,40 @@ "node": ">=8" } }, + "node_modules/redis": { + "version": "4.6.10", + "resolved": "https://registry.npmjs.org/redis/-/redis-4.6.10.tgz", + "integrity": "sha512-mmbyhuKgDiJ5TWUhiKhBssz+mjsuSI/lSZNPI9QvZOYzWvYGejtb+W3RlDDf8LD6Bdl5/mZeG8O1feUGhXTxEg==", + "optional": true, + "peer": true, + "dependencies": { + "@redis/bloom": "1.2.0", + "@redis/client": "1.5.11", + "@redis/graph": "1.1.0", + "@redis/json": "1.0.6", + "@redis/search": "1.1.5", + "@redis/time-series": "1.0.5" + } + }, + "node_modules/redis-errors": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz", + "integrity": "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==", + "engines": { + "node": ">=4" + } + }, + "node_modules/redis-parser": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-3.0.0.tgz", + "integrity": "sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==", + "dependencies": { + "redis-errors": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/reflect.getprototypeof": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.4.tgz", @@ -21397,6 +21592,11 @@ "node": ">=8" } }, + "node_modules/standard-as-callback": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz", + "integrity": "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==" + }, "node_modules/statuses": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", diff --git a/package.json b/package.json index 3a9578c8eb..eb6401d37b 100644 --- a/package.json +++ b/package.json @@ -66,7 +66,7 @@ }, "homepage": "https://github.com/danny-avila/LibreChat#readme", "devDependencies": { - "@playwright/test": "^1.32.1", + "@playwright/test": "^1.38.1", "@typescript-eslint/eslint-plugin": "^5.62.0", "@typescript-eslint/parser": "^5.62.0", "cross-env": "^7.0.3",