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 46 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: 0 additions & 1 deletion client/src/components/Auth/LoginForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ type TLoginFormProps = {
const LoginForm: React.FC<TLoginFormProps> = ({ onSubmit, startupConfig, error, setError }) => {
const localize = useLocalize();
const { theme } = useContext(ThemeContext);

const {
register,
getValues,
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 @@ export default function Settings({ open, onOpenChange }: TDialogProps) {
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 @@ export default function Settings({ open, onOpenChange }: TDialogProps) {
<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
129 changes: 129 additions & 0 deletions client/src/components/Nav/SettingsTabs/Balance/AutoRefillSettings.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import React from 'react';
import { TranslationKeys, useLocalize } from '~/hooks';
import { Label } from '~/components';
import HoverCardSettings from '~/components/Nav/SettingsTabs/HoverCardSettings';

interface AutoRefillSettingsProps {
lastRefill: Date;
refillAmount: number;
refillIntervalUnit: 'seconds' | 'minutes' | 'hours' | 'days' | 'weeks' | 'months';
refillIntervalValue: number;
}

/**
* Adds a time interval to a given date.
* @param {Date} date - The starting date.
* @param {number} value - The numeric value of the interval.
* @param {'seconds'|'minutes'|'hours'|'days'|'weeks'|'months'} unit - The unit of time.
* @returns {Date} A new Date representing the starting date plus the interval.
*/
const addIntervalToDate = (
date: Date,
value: number,
unit: 'seconds' | 'minutes' | 'hours' | 'days' | 'weeks' | 'months',
): Date => {
const result = new Date(date);
switch (unit) {
case 'seconds':
result.setSeconds(result.getSeconds() + value);
break;
case 'minutes':
result.setMinutes(result.getMinutes() + value);
break;
case 'hours':
result.setHours(result.getHours() + value);
break;
case 'days':
result.setDate(result.getDate() + value);
break;
case 'weeks':
result.setDate(result.getDate() + value * 7);
break;
case 'months':
result.setMonth(result.getMonth() + value);
break;
default:
break;
}
return result;
};

const AutoRefillSettings: React.FC<AutoRefillSettingsProps> = ({
lastRefill,
refillAmount,
refillIntervalUnit,
refillIntervalValue,
}) => {
const localize = useLocalize();

const lastRefillDate = lastRefill ? new Date(lastRefill) : null;
const nextRefill = lastRefillDate
? addIntervalToDate(lastRefillDate, refillIntervalValue, refillIntervalUnit)
: null;

// Return the localized unit based on singular/plural values
const getLocalizedIntervalUnit = (
value: number,
unit: 'seconds' | 'minutes' | 'hours' | 'days' | 'weeks' | 'months',
): string => {
let key: TranslationKeys;
switch (unit) {
case 'seconds':
key = value === 1 ? 'com_nav_balance_second' : 'com_nav_balance_seconds';
break;
case 'minutes':
key = value === 1 ? 'com_nav_balance_minute' : 'com_nav_balance_minutes';
break;
case 'hours':
key = value === 1 ? 'com_nav_balance_hour' : 'com_nav_balance_hours';
break;
case 'days':
key = value === 1 ? 'com_nav_balance_day' : 'com_nav_balance_days';
break;
case 'weeks':
key = value === 1 ? 'com_nav_balance_week' : 'com_nav_balance_weeks';
break;
case 'months':
key = value === 1 ? 'com_nav_balance_month' : 'com_nav_balance_months';
break;
default:
key = 'com_nav_balance_seconds';
}
return localize(key);
};

return (
<div className="space-y-4">
<h3 className="text-lg font-medium">{localize('com_nav_balance_auto_refill_settings')}</h3>
<div className="mb-1 flex justify-between text-sm">
<span>{localize('com_nav_balance_last_refill')}</span>
<span>{lastRefillDate ? lastRefillDate.toLocaleString() : '-'}</span>
</div>
<div className="mb-1 flex justify-between text-sm">
<span>{localize('com_nav_balance_refill_amount')}</span>
<span>{refillAmount !== undefined ? refillAmount : '-'}</span>
</div>
<div className="mb-1 flex justify-between text-sm">
<span>{localize('com_nav_balance_interval')}</span>
<span>
{localize('com_nav_balance_every')} {refillIntervalValue}{' '}
{getLocalizedIntervalUnit(refillIntervalValue, refillIntervalUnit)}
</span>
</div>
<div className="flex items-center justify-between">
{/* Left Section: Label */}
<div className="flex items-center space-x-2">
<Label className="font-light">{localize('com_nav_balance_next_refill')}</Label>
<HoverCardSettings side="bottom" text="com_nav_balance_next_refill_info" />
</div>

{/* Right Section: tokenCredits Value */}
<span className="text-sm font-medium text-gray-800 dark:text-gray-200" role="note">
{nextRefill ? nextRefill.toLocaleString() : '-'}
</span>
</div>
</div>
);
};

export default AutoRefillSettings;
40 changes: 40 additions & 0 deletions client/src/components/Nav/SettingsTabs/Balance/Balance.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import React from 'react';
import { useGetStartupConfig, useGetUserBalance } from '~/data-provider';
import { useAuthContext, useLocalize } from '~/hooks';
import TokenCreditsItem from './TokenCreditsItem';
import AutoRefillSettings from './AutoRefillSettings';

function Balance() {
const localize = useLocalize();
const { isAuthenticated } = useAuthContext();
const { data: startupConfig } = useGetStartupConfig();
const balanceQuery = useGetUserBalance({
enabled: !!isAuthenticated && startupConfig?.balance?.enabled,
});
const balanceData = balanceQuery.data;

return (
<div className="flex flex-col gap-4 p-4 text-sm text-text-primary">
{/* Token credits display */}
<TokenCreditsItem tokenCredits={balanceData?.tokenCredits} />

{/* Auto-refill display */}
{balanceData?.autoRefillEnabled ? (
<>
<AutoRefillSettings
lastRefill={balanceData.lastRefill!}
Copy link
Preview

Copilot AI May 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using non-null assertions on auto-refill properties (lastRefill, refillAmount, refillIntervalUnit, refillIntervalValue) can lead to runtime errors if any of these fields are unexpectedly undefined. Consider adding explicit runtime checks or providing default values before rendering the AutoRefillSettings component.

Copilot uses AI. Check for mistakes.

refillAmount={balanceData.refillAmount!}
refillIntervalUnit={balanceData.refillIntervalUnit!}
refillIntervalValue={balanceData.refillIntervalValue!}
/>
</>
) : (
<div className="text-sm text-gray-600">
{localize('com_nav_balance_auto_refill_disabled')}
</div>
)}
</div>
);
}

export default React.memo(Balance);
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import React from 'react';
import { useLocalize } from '~/hooks';
import { Label } from '~/components';
import HoverCardSettings from '~/components/Nav/SettingsTabs/HoverCardSettings';

interface TokenCreditsItemProps {
tokenCredits?: number;
}

const TokenCreditsItem: React.FC<TokenCreditsItemProps> = ({ tokenCredits }) => {
const localize = useLocalize();

return (
<div className="flex items-center justify-between">
{/* Left Section: Label */}
<div className="flex items-center space-x-2">
<Label className="font-light">{localize('com_nav_balance')}</Label>
<HoverCardSettings side="bottom" text="com_nav_info_user_name_display" />
Copy link
Preview

Copilot AI May 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The translation key com_nav_info_user_name_display appears unrelated to token credits. Update it to a balance-related key (e.g., com_nav_balance_info).

Suggested change
<HoverCardSettings side="bottom" text="com_nav_info_user_name_display" />
<HoverCardSettings side="bottom" text="com_nav_balance_info" />

Copilot uses AI. Check for mistakes.

</div>

{/* Right Section: tokenCredits Value */}
<span className="text-sm font-medium text-gray-800 dark:text-gray-200" role="note">
{tokenCredits !== undefined ? tokenCredits.toFixed(2) : '0.00'}
</span>
</div>
);
};

export default TokenCreditsItem;
1 change: 1 addition & 0 deletions client/src/components/Nav/SettingsTabs/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@ export { default as Beta } from './Beta/Beta';
export { default as Commands } from './Commands/Commands';
export { RevokeKeysButton } from './Data/RevokeKeysButton';
export { default as Account } from './Account/Account';
export { default as Balance } from './Balance/Balance';
export { default as Speech } from './Speech/Speech';
6 changes: 3 additions & 3 deletions client/src/data-provider/Misc/queries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,10 @@ export const useGetBannerQuery = (
};

export const useGetUserBalance = (
config?: UseQueryOptions<string>,
): QueryObserverResult<string> => {
config?: UseQueryOptions<t.TBalanceResponse>,
): QueryObserverResult<t.TBalanceResponse> => {
const queriesEnabled = useRecoilValue<boolean>(store.queriesEnabled);
return useQuery<string>([QueryKeys.balance], () => dataService.getUserBalance(), {
return useQuery<t.TBalanceResponse>([QueryKeys.balance], () => dataService.getUserBalance(), {
refetchOnWindowFocus: true,
refetchOnReconnect: true,
refetchOnMount: true,
Expand Down
23 changes: 22 additions & 1 deletion client/src/locales/en/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,26 @@
"com_nav_auto_transcribe_audio": "Auto transcribe audio",
"com_nav_automatic_playback": "Autoplay Latest Message",
"com_nav_balance": "Balance",
"com_nav_balance_auto_refill_settings": "Auto-Refill Settings",
"com_nav_balance_auto_refill_disabled": "Auto-Refill is disabled.",
"com_nav_balance_last_refill": "Last Refill:",
"com_nav_balance_refill_amount": "Refill Amount:",
"com_nav_balance_interval": "Interval:",
"com_nav_balance_next_refill": "Next Refill:",
"com_nav_balance_next_refill_info": "The next refill will occur automatically only when both conditions are met: the designated time interval has passed since the last refill, and sending a prompt would cause your balance to drop below zero.",
"com_nav_balance_every": "Every",
"com_nav_balance_second": "second",
"com_nav_balance_seconds": "seconds",
"com_nav_balance_minute": "minute",
"com_nav_balance_minutes": "minutes",
"com_nav_balance_hour": "hour",
"com_nav_balance_hours": "hours",
"com_nav_balance_day": "day",
"com_nav_balance_days": "days",
"com_nav_balance_week": "week",
"com_nav_balance_weeks": "weeks",
"com_nav_balance_month": "month",
"com_nav_balance_months": "months",
"com_nav_browser": "Browser",
"com_nav_center_chat_input": "Center Chat Input on Welcome Screen",
"com_nav_change_picture": "Change picture",
Expand Down Expand Up @@ -416,6 +436,7 @@
"com_nav_search_placeholder": "Search messages",
"com_nav_send_message": "Send message",
"com_nav_setting_account": "Account",
"com_nav_setting_balance": "Balance",
"com_nav_setting_beta": "Beta features",
"com_nav_setting_chat": "Chat",
"com_nav_setting_data": "Data controls",
Expand Down Expand Up @@ -926,4 +947,4 @@
"com_ui_zoom": "Zoom",
"com_user_message": "You",
"com_warning_resubmit_unsupported": "Resubmitting the AI message is not supported for this endpoint."
}
}
4 changes: 4 additions & 0 deletions packages/data-provider/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1275,6 +1275,10 @@ export enum SettingsTabValues {
* Tab for Data Controls
*/
DATA = 'data',
/**
* Tab for Balance Settings
*/
BALANCE = 'balance',
/**
* Tab for Account Settings
*/
Expand Down
2 changes: 1 addition & 1 deletion packages/data-provider/src/data-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ export function getUser(): Promise<t.TUser> {
return request.get(endpoints.user());
}

export function getUserBalance(): Promise<string> {
export function getUserBalance(): Promise<t.TBalanceResponse> {
return request.get(endpoints.balance());
}

Expand Down
Loading
Loading