Skip to content

💸 feat: Balance Tab in Settings Dialog #6537

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 47 commits into from
May 29, 2025
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
d0090d8
🚀 feat: Implement Auto-Refill Settings for Balance
rubentalstra Mar 26, 2025
91ec113
Merge branch 'main' into feat/balance-settings-tab
rubentalstra Mar 26, 2025
da32349
Merge branch 'main' into feat/balance-settings-tab
rubentalstra Mar 27, 2025
e3acd18
🎨 feat: add `copy-tex` to improve copying KaTeX (#7308)
andresgit May 15, 2025
71effb1
🔃 refactor: `AgentFooter` to conditionally render buttons based on `a…
mawburn May 15, 2025
c925f9f
🚀 feat: Add `Cloudflare Turnstile` support (#5987)
rubentalstra May 15, 2025
6128270
Merge branch 'main' into feat/balance-settings-tab
rubentalstra May 15, 2025
a2d5c07
Merge branch 'dev' into feat/balance-settings-tab
rubentalstra May 15, 2025
a370c19
Potential fix for code scanning alert no. 5764: Ensure code is proper…
rubentalstra May 15, 2025
cad4489
Potential fix for code scanning alert no. 5765: Ensure code is proper…
rubentalstra May 15, 2025
d7f9fe1
🖼️ feat: Tool Call and Loading UI Refresh, Image Resize Config (#7086)
berry-13 May 16, 2025
ceccfb0
Merge branch 'dev' into feat/balance-settings-tab
rubentalstra May 19, 2025
a849ff8
📊 feat: Improve Helm Chart (#3638)
hofq May 17, 2025
a10e754
📜 docs: Unreleased Changelog (#7434)
github-actions[bot] May 19, 2025
3cbae93
🛡️ chore: `multer` v2.0.0 for CVE-2025-47935 and CVE-2025-47944 (#7454)
danny-avila May 19, 2025
b812cb1
🎚️ feat: Custom Parameters (#7342)
nhtruong May 19, 2025
b2ea660
📃 fix: Ensure MCP Resources Pass Name and Description Fields to LLM (…
renehonig May 19, 2025
cd1b6bf
🔗 feat: Support Environment Variables in MCP URL Config (#7424)
benverhees May 19, 2025
a4b5234
🦙 chore: Add `llama-4` to Vision Models List (#7433)
AmgadHasan May 19, 2025
5c0fbe4
🔧 fix: File Deletion for Azure Assistants API (#7466)
danny-avila May 20, 2025
00b0313
🔬 fix: File Search Request Format (Azure Assistants API) (#7404)
arthurolivierfortin May 20, 2025
7929a6f
🖼️ chore: Linting & Transition Styling in UI Components (#7467)
danny-avila May 20, 2025
fbbd0d0
✅ fix: Emojis rendering in `SplitText` Animation (#7460)
sbruel May 20, 2025
ee86a11
📂 refactor: Improve `FileAttachment` & File Form Deletion (#7471)
danny-avila May 20, 2025
91da375
🌍 i18n: Update translation.json with latest translations (#7468)
github-actions[bot] May 20, 2025
696298d
🦾 feat: Claude-4 Support (#7509)
danny-avila May 22, 2025
ed37d84
📊 chore: Remove Old Helm Chart (#7512)
hofq May 23, 2025
422a64d
🪨 feat: Bedrock Support for Claude-4 Reasoning (#7517)
danny-avila May 23, 2025
2663aa2
🪖 chore: bump helm app version to v0.7.8 (#7524)
austin-barrington May 23, 2025
a49c1ff
⌛ feat: Agent Version History and Management (#7455)
mawburn May 20, 2025
c34dd83
*️⃣ feat: Reuse OpenID Auth Tokens (#7397)
peeeteeer May 22, 2025
2d5465a
🔎 feat: Native Web Search with Citation References (#7516)
danny-avila May 23, 2025
ef861d7
🧹 chore: Bump Agents Dependencies (#7525)
danny-avila May 23, 2025
09dacff
🔧 refactor: Progress Text Localization for Running Tools (#7526)
danny-avila May 23, 2025
2786aed
🔧 chore: Bump Data Provider and Custom Config Versions (#7527)
danny-avila May 23, 2025
5422dcd
👤 feat: Enhance Agent Versioning to Track User Updates (#7523)
mawburn May 24, 2025
ca463ea
🧩 feat: Web Search Config Validations & Clipboard Citation Processing…
danny-avila May 24, 2025
e2fdfc1
🌍 i18n: Update translation.json with latest translations (#7532)
github-actions[bot] May 24, 2025
20034cc
🔧 chore: Update data-provider dependencies for typing (#7533)
danny-avila May 24, 2025
3a73031
🔧 fix: Artifacts Display Crash on Close and Max Width (#7540)
danny-avila May 24, 2025
6a44e0a
🏷️ refactor: EditPresetDialog UI and Remove `chatGptLabel` from Prese…
danny-avila May 24, 2025
0a33546
📦 refactor: Add Additional Chunking to Vite Config (#7544)
danny-avila May 24, 2025
e99dde2
⌚ fix: Debounce `setUserContext` and Default State Param for OpenID A…
danny-avila May 26, 2025
79ee302
🚀 feat: Implement Auto-Refill Settings for Balance
rubentalstra Mar 26, 2025
8b6b82d
Merge branch 'dev' into feat/balance-settings-tab
rubentalstra May 26, 2025
42acd7e
fix: ESLint
rubentalstra May 26, 2025
2f19ca0
✨ feat: Enhance Auto-Refill Settings with Validation and Localization
rubentalstra May 26, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 18 additions & 3 deletions api/server/controllers/Balance.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,24 @@
const Balance = require('~/models/Balance');

async function balanceController(req, res) {
const { tokenCredits: balance = '' } =
(await Balance.findOne({ user: req.user.id }, 'tokenCredits').lean()) ?? {};
res.status(200).send('' + balance);
const balanceData = await Balance.findOne(
{ user: req.user.id },
'-_id tokenCredits autoRefillEnabled refillIntervalValue refillIntervalUnit lastRefill refillAmount',
).lean();

if (!balanceData) {
return res.status(404).json({ error: 'Balance not found' });
}

// If auto-refill is not enabled, remove auto-refill related fields from the response
if (!balanceData.autoRefillEnabled) {
delete balanceData.refillIntervalValue;
delete balanceData.refillIntervalUnit;
delete balanceData.lastRefill;
delete balanceData.refillAmount;
}

res.status(200).json(balanceData);
}

module.exports = balanceController;
1 change: 1 addition & 0 deletions api/server/routes/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
4 changes: 3 additions & 1 deletion api/server/services/AppService.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand All @@ -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.
Expand Down Expand Up @@ -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,
Expand All @@ -85,6 +86,7 @@ const AppService = async (app) => {
availableTools,
imageOutputType,
interfaceConfig,
turnstileConfig,
balance,
};

Expand Down
11 changes: 11 additions & 0 deletions api/server/services/AppService.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,12 @@ jest.mock('./ToolService', () => ({
},
}),
}));
jest.mock('./start/turnstile', () => ({
loadTurnstileConfig: jest.fn(() => ({
siteKey: 'default-site-key',
options: {},
})),
}));

const azureGroups = [
{
Expand Down Expand Up @@ -86,6 +92,10 @@ const azureGroups = [

describe('AppService', () => {
let app;
const mockedTurnstileConfig = {
siteKey: 'default-site-key',
options: {},
};

beforeEach(() => {
app = { locals: {} };
Expand All @@ -107,6 +117,7 @@ describe('AppService', () => {
sidePanel: true,
presets: true,
}),
turnstileConfig: mockedTurnstileConfig,
modelSpecs: undefined,
availableTools: {
ExampleTool: {
Expand Down
35 changes: 35 additions & 0 deletions api/server/services/start/turnstile.js
Original file line number Diff line number Diff line change
@@ -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,
};
1 change: 1 addition & 0 deletions client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
26 changes: 24 additions & 2 deletions client/src/components/Auth/LoginForm.tsx
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -14,16 +15,19 @@ type TLoginFormProps = {

const LoginForm: React.FC<TLoginFormProps> = ({ onSubmit, startupConfig, error, setError }) => {
const localize = useLocalize();
const { theme } = useContext(ThemeContext);
const {
register,
getValues,
handleSubmit,
formState: { errors },
} = useForm<TLoginUser>();
const [showResendLink, setShowResendLink] = useState<boolean>(false);
const [turnstileToken, setTurnstileToken] = useState<string | null>(null);

const { data: config } = useGetStartupConfig();
const useUsernameLogin = config?.ldap?.username;
const validTheme = theme === 'dark' ? 'dark' : 'light';

useEffect(() => {
if (error && error.includes('422') && !showResendLink) {
Expand Down Expand Up @@ -159,11 +163,29 @@ const LoginForm: React.FC<TLoginFormProps> = ({ onSubmit, startupConfig, error,
{localize('com_auth_password_forgot')}
</a>
)}

{/* Render Turnstile only if enabled in startupConfig */}
{startupConfig.turnstile && (
<div className="my-4 flex justify-center">
<Turnstile
siteKey={startupConfig.turnstile.siteKey}
options={{
...startupConfig.turnstile.options,
theme: validTheme,
}}
onSuccess={(token) => setTurnstileToken(token)}
onError={() => setTurnstileToken(null)}
onExpire={() => setTurnstileToken(null)}
/>
</div>
)}

<div className="mt-6">
<button
aria-label={localize('com_auth_continue')}
data-testid="login-button"
type="submit"
disabled={startupConfig.turnstile ? !turnstileToken : false}
className="
w-full rounded-2xl bg-green-600 px-4 py-3 text-sm font-medium text-white
transition-colors hover:bg-green-700 dark:bg-green-600 dark:hover:bg-green-700
Expand Down
41 changes: 33 additions & 8 deletions client/src/components/Auth/Registration.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
import { useForm } from 'react-hook-form';
import React, { useState } from 'react';
import React, { useContext, useState } from 'react';
import { Turnstile } from '@marsidev/react-turnstile';
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 type { TLoginLayoutContext } from '~/common';
import { ErrorMessage } from './ErrorMessage';
import { Spinner } from '~/components/svg';
import { useLocalize, TranslationKeys } from '~/hooks';
import { useLocalize, TranslationKeys, ThemeContext } from '~/hooks';

const Registration: React.FC = () => {
const navigate = useNavigate();
const localize = useLocalize();
const { theme } = useContext(ThemeContext);
const { startupConfig, startupConfigError, isFetching } = useOutletContext<TLoginLayoutContext>();

const {
Expand All @@ -24,10 +26,12 @@ const Registration: React.FC = () => {
const [errorMessage, setErrorMessage] = useState<string>('');
const [isSubmitting, setIsSubmitting] = useState(false);
const [countdown, setCountdown] = useState<number>(3);
const [turnstileToken, setTurnstileToken] = useState<string | null>(null);

const location = useLocation();
const queryParams = new URLSearchParams(location.search);
const token = queryParams.get('token');
const validTheme = theme === 'dark' ? 'dark' : 'light';

const registerUser = useRegisterUserMutation({
onMutate: () => {
Expand Down Expand Up @@ -178,17 +182,38 @@ const Registration: React.FC = () => {
validate: (value: string) =>
value === password || localize('com_auth_password_not_match'),
})}

{/* Render Turnstile only if enabled in startupConfig */}
{startupConfig?.turnstile && (
<div className="my-4 flex justify-center">
<Turnstile
siteKey={startupConfig.turnstile.siteKey}
options={{
...startupConfig.turnstile.options,
theme: validTheme,
}}
onSuccess={(token) => setTurnstileToken(token)}
onError={() => setTurnstileToken(null)}
onExpire={() => setTurnstileToken(null)}
/>
</div>
)}

<div className="mt-6">
<button
disabled={Object.keys(errors).length > 0}
disabled={
Object.keys(errors).length > 0 ||
isSubmitting ||
(startupConfig?.turnstile ? !turnstileToken : false)
}
type="submit"
aria-label="Submit registration"
className="
w-full rounded-2xl bg-green-600 px-4 py-3 text-sm font-medium text-white
transition-colors hover:bg-green-700 focus:outline-none focus:ring-2
focus:ring-green-500 focus:ring-offset-2 disabled:opacity-50
disabled:hover:bg-green-600 dark:bg-green-600 dark:hover:bg-green-700
"
w-full rounded-2xl bg-green-600 px-4 py-3 text-sm font-medium text-white
transition-colors hover:bg-green-700 focus:outline-none focus:ring-2
focus:ring-green-500 focus:ring-offset-2 disabled:opacity-50
disabled:hover:bg-green-600 dark:bg-green-600 dark:hover:bg-green-700
"
>
{isSubmitting ? <Spinner /> : localize('com_auth_continue')}
</button>
Expand Down
6 changes: 2 additions & 4 deletions client/src/components/Nav/AccountSettings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -75,12 +75,10 @@ function AccountSettings() {
{user?.email ?? localize('com_nav_user')}
</div>
<DropdownMenuSeparator />
{startupConfig?.balance?.enabled === true &&
balanceQuery.data != null &&
!isNaN(parseFloat(balanceQuery.data)) && (
{startupConfig?.balance?.enabled === true && balanceQuery.data != null && (
<>
<div className="text-token-text-secondary ml-3 mr-2 py-2 text-sm" role="note">
{localize('com_nav_balance')}: {parseFloat(balanceQuery.data).toFixed(2)}
{localize('com_nav_balance')}: {balanceQuery.data.tokenCredits.toFixed(2)}
</div>
<DropdownMenuSeparator />
</>
Expand Down
23 changes: 20 additions & 3 deletions client/src/components/Nav/Settings.tsx
Original file line number Diff line number Diff line change
@@ -1,28 +1,31 @@
import React, { useState, useRef } from 'react';
import * as Tabs from '@radix-ui/react-tabs';
import { MessageSquare, Command } from 'lucide-react';
import { MessageSquare, Command, DollarSign } from 'lucide-react';
import { SettingsTabValues } from 'librechat-data-provider';
import { useGetStartupConfig } from '~/data-provider';
import type { TDialogProps } from '~/common';
import { Dialog, DialogPanel, DialogTitle, Transition, TransitionChild } from '@headlessui/react';
import { GearIcon, DataIcon, SpeechIcon, UserIcon, ExperimentIcon } from '~/components/svg';
import { General, Chat, Speech, Beta, Commands, Data, Account } from './SettingsTabs';
import { General, Chat, Speech, Beta, Commands, Data, Account, Balance } from './SettingsTabs';
import { useMediaQuery, useLocalize, TranslationKeys } from '~/hooks';
import { cn } from '~/utils';

export default function Settings({ open, onOpenChange }: TDialogProps) {
const isSmallScreen = useMediaQuery('(max-width: 767px)');
const { data: startupConfig } = useGetStartupConfig();
const localize = useLocalize();
const [activeTab, setActiveTab] = useState(SettingsTabValues.GENERAL);
const tabRefs = useRef({});

const handleKeyDown = (event: React.KeyboardEvent) => {
const tabs = [
const tabs: SettingsTabValues[] = [
SettingsTabValues.GENERAL,
SettingsTabValues.CHAT,
SettingsTabValues.BETA,
SettingsTabValues.COMMANDS,
SettingsTabValues.SPEECH,
SettingsTabValues.DATA,
...(startupConfig?.balance?.enabled ? [SettingsTabValues.BALANCE] : []),
SettingsTabValues.ACCOUNT,
];
const currentIndex = tabs.indexOf(activeTab);
Expand Down Expand Up @@ -82,6 +85,15 @@
icon: <DataIcon />,
label: 'com_nav_setting_data',
},
...(startupConfig?.balance?.enabled
? [
{
value: SettingsTabValues.BALANCE,
icon: <DollarSign size={18} />,
label: 'com_nav_setting_balance' as TranslationKeys,
},
]
: ([] as { value: SettingsTabValues; icon: React.JSX.Element; label: TranslationKeys }[])),
{
value: SettingsTabValues.ACCOUNT,
icon: <UserIcon />,
Expand Down Expand Up @@ -204,6 +216,11 @@
<Tabs.Content value={SettingsTabValues.DATA}>
<Data />
</Tabs.Content>
{startupConfig?.balance?.enabled && (
<Tabs.Content value={SettingsTabValues.BALANCE}>
<Balance />
</Tabs.Content>
)}
<Tabs.Content value={SettingsTabValues.ACCOUNT}>
<Account />
</Tabs.Content>
Expand Down
Loading