diff --git a/CHANGELOG.md b/CHANGELOG.md index 1b602cba2..eb4c65c3a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,28 @@ All notable changes to this project will be documented in this file. + +## [Unreleased] + +### ✨ New Features + +- ✨ feat: implement search parameter updates by **@mawburn** in [#7151](https://github.com/danny-avila/LibreChat/pull/7151) +- 🎏 feat: Add MCP support for Streamable HTTP Transport by **@benverhees** in [#7353](https://github.com/danny-avila/LibreChat/pull/7353) + +### 🔧 Fixes + +- 💬 fix: update aria-label for accessibility in ConvoLink component by **@berry-13** in [#7320](https://github.com/danny-avila/LibreChat/pull/7320) +- 🔑 fix: use `apiKey` instead of `openAIApiKey` in OpenAI-like Config by **@danny-avila** in [#7337](https://github.com/danny-avila/LibreChat/pull/7337) +- 🔄 fix: update navigation logic in `useFocusChatEffect` to ensure correct search parameters are used by **@mawburn** in [#7340](https://github.com/danny-avila/LibreChat/pull/7340) + +### ⚙️ Other Changes + +- 📜 docs: CHANGELOG for release v0.7.8 by **@github-actions[bot]** in [#7290](https://github.com/danny-avila/LibreChat/pull/7290) +- 📦 chore: Update API Package Dependencies by **@danny-avila** in [#7359](https://github.com/danny-avila/LibreChat/pull/7359) + + + +--- ## [v0.7.8] - Changes from v0.7.8-rc1 to v0.7.8. @@ -45,6 +67,7 @@ Changes from v0.7.8-rc1 to v0.7.8. --- ## [v0.7.8-rc1] - +## [v0.7.8-rc1] - Changes from v0.7.7 to v0.7.8-rc1. diff --git a/api/server/routes/config.js b/api/server/routes/config.js index ebafb05c3..583453fe4 100644 --- a/api/server/routes/config.js +++ b/api/server/routes/config.js @@ -75,6 +75,7 @@ router.get('/', async function (req, res) { process.env.SHOW_BIRTHDAY_ICON === '', helpAndFaqURL: process.env.HELP_AND_FAQ_URL || 'https://librechat.ai', interface: req.app.locals.interfaceConfig, + turnstile: req.app.locals.turnstileConfig, modelSpecs: req.app.locals.modelSpecs, balance: req.app.locals.balance, sharedLinksEnabled, diff --git a/api/server/services/AppService.js b/api/server/services/AppService.js index 1ad3aaace..5f119e67a 100644 --- a/api/server/services/AppService.js +++ b/api/server/services/AppService.js @@ -12,6 +12,7 @@ const { initializeFirebase } = require('./Files/Firebase/initialize'); const loadCustomConfig = require('./Config/loadCustomConfig'); const handleRateLimits = require('./Config/handleRateLimits'); const { loadDefaultInterface } = require('./start/interface'); +const { loadTurnstileConfig } = require('./start/turnstile'); const { azureConfigSetup } = require('./start/azureOpenAI'); const { processModelSpecs } = require('./start/modelSpecs'); const { initializeS3 } = require('./Files/S3/initialize'); @@ -23,7 +24,6 @@ const { getMCPManager } = require('~/config'); const paths = require('~/config/paths'); /** - * * Loads custom config and initializes app-wide variables. * @function AppService * @param {Express.Application} app - The Express application object. @@ -74,6 +74,7 @@ const AppService = async (app) => { const socialLogins = config?.registration?.socialLogins ?? configDefaults?.registration?.socialLogins; const interfaceConfig = await loadDefaultInterface(config, configDefaults); + const turnstileConfig = loadTurnstileConfig(config, configDefaults); const defaultLocals = { ocr, @@ -85,6 +86,7 @@ const AppService = async (app) => { availableTools, imageOutputType, interfaceConfig, + turnstileConfig, balance, }; diff --git a/api/server/services/AppService.spec.js b/api/server/services/AppService.spec.js index 465ec9fdd..81a017e41 100644 --- a/api/server/services/AppService.spec.js +++ b/api/server/services/AppService.spec.js @@ -46,6 +46,12 @@ jest.mock('./ToolService', () => ({ }, }), })); +jest.mock('./start/turnstile', () => ({ + loadTurnstileConfig: jest.fn(() => ({ + siteKey: 'default-site-key', + options: {}, + })), +})); const azureGroups = [ { @@ -86,6 +92,10 @@ const azureGroups = [ describe('AppService', () => { let app; + const mockedTurnstileConfig = { + siteKey: 'default-site-key', + options: {}, + }; beforeEach(() => { app = { locals: {} }; @@ -107,6 +117,7 @@ describe('AppService', () => { sidePanel: true, presets: true, }), + turnstileConfig: mockedTurnstileConfig, modelSpecs: undefined, availableTools: { ExampleTool: { diff --git a/api/server/services/start/turnstile.js b/api/server/services/start/turnstile.js new file mode 100644 index 000000000..ffd4545da --- /dev/null +++ b/api/server/services/start/turnstile.js @@ -0,0 +1,35 @@ +const { removeNullishValues } = require('librechat-data-provider'); +const { logger } = require('~/config'); + +/** + * Loads and maps the Cloudflare Turnstile configuration. + * + * Expected config structure: + * + * turnstile: + * siteKey: "your-site-key-here" + * options: + * language: "auto" // "auto" or an ISO 639-1 language code (e.g. en) + * size: "normal" // Options: "normal", "compact", "flexible", or "invisible" + * + * @param {TCustomConfig | undefined} config - The loaded custom configuration. + * @param {TConfigDefaults} configDefaults - The custom configuration default values. + * @returns {TCustomConfig['turnstile']} The mapped Turnstile configuration. + */ +function loadTurnstileConfig(config, configDefaults) { + const { turnstile: customTurnstile = {} } = config ?? {}; + const { turnstile: defaults = {} } = configDefaults; + + /** @type {TCustomConfig['turnstile']} */ + const loadedTurnstile = removeNullishValues({ + siteKey: customTurnstile.siteKey ?? defaults.siteKey, + options: customTurnstile.options ?? defaults.options, + }); + + logger.info('Turnstile configuration loaded:\n' + JSON.stringify(loadedTurnstile, null, 2)); + return loadedTurnstile; +} + +module.exports = { + loadTurnstileConfig, +}; diff --git a/client/package.json b/client/package.json index 5fd9729a7..1b33c3791 100644 --- a/client/package.json +++ b/client/package.json @@ -34,6 +34,7 @@ "@dicebear/collection": "^9.2.2", "@dicebear/core": "^9.2.2", "@headlessui/react": "^2.1.2", + "@marsidev/react-turnstile": "^1.1.0", "@radix-ui/react-accordion": "^1.1.2", "@radix-ui/react-alert-dialog": "^1.0.2", "@radix-ui/react-checkbox": "^1.0.3", diff --git a/client/src/components/Auth/LoginForm.tsx b/client/src/components/Auth/LoginForm.tsx index 2cd62d08b..030b6323f 100644 --- a/client/src/components/Auth/LoginForm.tsx +++ b/client/src/components/Auth/LoginForm.tsx @@ -1,9 +1,10 @@ import { useForm } from 'react-hook-form'; -import React, { useState, useEffect } from 'react'; +import React, { useState, useEffect, useContext } from 'react'; +import { Turnstile } from '@marsidev/react-turnstile'; import type { TLoginUser, TStartupConfig } from 'librechat-data-provider'; import type { TAuthContext } from '~/common'; import { useResendVerificationEmail, useGetStartupConfig } from '~/data-provider'; -import { useLocalize } from '~/hooks'; +import { ThemeContext, useLocalize } from '~/hooks'; type TLoginFormProps = { onSubmit: (data: TLoginUser) => void; @@ -14,6 +15,7 @@ type TLoginFormProps = { const LoginForm: React.FC = ({ onSubmit, startupConfig, error, setError }) => { const localize = useLocalize(); + const { theme } = useContext(ThemeContext); const { register, getValues, @@ -21,9 +23,11 @@ const LoginForm: React.FC = ({ onSubmit, startupConfig, error, formState: { errors }, } = useForm(); const [showResendLink, setShowResendLink] = useState(false); + const [turnstileToken, setTurnstileToken] = useState(null); const { data: config } = useGetStartupConfig(); const useUsernameLogin = config?.ldap?.username; + const validTheme = theme === 'dark' ? 'dark' : 'light'; useEffect(() => { if (error && error.includes('422') && !showResendLink) { @@ -159,11 +163,29 @@ const LoginForm: React.FC = ({ onSubmit, startupConfig, error, {localize('com_auth_password_forgot')} )} + + {/* Render Turnstile only if enabled in startupConfig */} + {startupConfig.turnstile && ( +
+ setTurnstileToken(token)} + onError={() => setTurnstileToken(null)} + onExpire={() => setTurnstileToken(null)} + /> +
+ )} +
+ )} +
diff --git a/client/src/components/Nav/SettingsTabs/General/General.tsx b/client/src/components/Nav/SettingsTabs/General/General.tsx index 3fe81726b..bba0f4088 100644 --- a/client/src/components/Nav/SettingsTabs/General/General.tsx +++ b/client/src/components/Nav/SettingsTabs/General/General.tsx @@ -80,8 +80,10 @@ export const LangSelector = ({ { value: 'zh-Hans', label: localize('com_nav_lang_chinese') }, { value: 'zh-Hant', label: localize('com_nav_lang_traditional_chinese') }, { value: 'ar-EG', label: localize('com_nav_lang_arabic') }, + { value: 'da-DK', label: localize('com_nav_lang_danish') }, { value: 'de-DE', label: localize('com_nav_lang_german') }, { value: 'es-ES', label: localize('com_nav_lang_spanish') }, + { value: 'ca-ES', label: localize('com_nav_lang_catalan') }, { value: 'et-EE', label: localize('com_nav_lang_estonian') }, { value: 'fa-IR', label: localize('com_nav_lang_persian') }, { value: 'fr-FR', label: localize('com_nav_lang_french') }, @@ -94,6 +96,7 @@ export const LangSelector = ({ { value: 'ru-RU', label: localize('com_nav_lang_russian') }, { value: 'ja-JP', label: localize('com_nav_lang_japanese') }, { value: 'ka-GE', label: localize('com_nav_lang_georgian') }, + { value: 'cs-CZ', label: localize('com_nav_lang_czech') }, { value: 'sv-SE', label: localize('com_nav_lang_swedish') }, { value: 'ko-KR', label: localize('com_nav_lang_korean') }, { value: 'vi-VN', label: localize('com_nav_lang_vietnamese') }, diff --git a/client/src/components/SidePanel/Agents/AgentFooter.tsx b/client/src/components/SidePanel/Agents/AgentFooter.tsx index 062f81d25..75f10a385 100644 --- a/client/src/components/SidePanel/Agents/AgentFooter.tsx +++ b/client/src/components/SidePanel/Agents/AgentFooter.tsx @@ -50,10 +50,12 @@ export default function AgentFooter({ return localize('com_ui_create'); }; + const showButtons = activePanel === Panel.builder; + return (
- {activePanel !== Panel.advanced && } - {user?.role === SystemRoles.ADMIN && } + {showButtons && } + {user?.role === SystemRoles.ADMIN && showButtons && } {/* Context Button */}
{(agent?.author === user?.id || user?.role === SystemRoles.ADMIN) && hasAccessToShareAgents && ( - - )} + + )} {agent && agent.author === user?.id && } {/* Submit Button */}