From 9c82eeddeb9f08934fd5f4103321fd52707c3b78 Mon Sep 17 00:00:00 2001 From: Junyan Qin Date: Mon, 29 Dec 2025 22:23:11 +0800 Subject: [PATCH] feat: add endpoint for retrieving user space credits and implement caching mechanism in UserService --- .../pkg/api/http/controller/groups/user.py | 6 ++ src/langbot/pkg/api/http/service/user.py | 26 ++++++ .../components/models-dialog/ModelsDialog.tsx | 84 +++++++++++++++---- web/src/app/infra/http/BackendClient.ts | 4 + web/src/i18n/locales/en-US.ts | 9 +- web/src/i18n/locales/ja-JP.ts | 9 +- web/src/i18n/locales/zh-Hans.ts | 9 +- web/src/i18n/locales/zh-Hant.ts | 9 +- 8 files changed, 125 insertions(+), 31 deletions(-) diff --git a/src/langbot/pkg/api/http/controller/groups/user.py b/src/langbot/pkg/api/http/controller/groups/user.py index 981b1bd8..91aa4f47 100644 --- a/src/langbot/pkg/api/http/controller/groups/user.py +++ b/src/langbot/pkg/api/http/controller/groups/user.py @@ -158,6 +158,12 @@ class UserRouterGroup(group.RouterGroup): } ) + @self.route('/space-credits', methods=['GET'], auth_type=group.AuthType.USER_TOKEN) + async def _(user_email: str) -> str: + """Get Space credits balance for current user""" + credits = await self.ap.user_service.get_space_credits(user_email) + return self.success(data={'credits': credits}) + @self.route('/account-info', methods=['GET'], auth_type=group.AuthType.NONE) async def _() -> str: """Get account info for login page (account type and has_password)""" diff --git a/src/langbot/pkg/api/http/service/user.py b/src/langbot/pkg/api/http/service/user.py index 6235ca1a..46410e9b 100644 --- a/src/langbot/pkg/api/http/service/user.py +++ b/src/langbot/pkg/api/http/service/user.py @@ -16,10 +16,12 @@ from ....utils import constants class UserService: ap: app.Application _create_user_lock: asyncio.Lock + _space_credits_cache: typing.Dict[str, typing.Tuple[int, float]] # {user_email: (credits, timestamp)} def __init__(self, ap: app.Application) -> None: self.ap = ap self._create_user_lock = asyncio.Lock() + self._space_credits_cache = {} def _get_space_config(self) -> typing.Dict[str, str]: """Get Space configuration from config file""" @@ -178,6 +180,30 @@ class UserService: raise ValueError(f'Failed to get user info: {data.get("msg")}') return data.get('data', {}) + async def get_space_credits(self, user_email: str, force_refresh: bool = False) -> int | None: + """Get Space credits for user with caching (60s TTL)""" + import time + + cache_ttl = 60 + + if not force_refresh and user_email in self._space_credits_cache: + credits, ts = self._space_credits_cache[user_email] + if time.time() - ts < cache_ttl: + return credits + + user_obj = await self.get_user_by_email(user_email) + if not user_obj or user_obj.account_type != 'space' or not user_obj.space_access_token: + return None + + try: + info = await self.get_space_user_info(user_obj.space_access_token) + credits = info.get('credits') + if credits is not None: + self._space_credits_cache[user_email] = (credits, time.time()) + return credits + except Exception: + return self._space_credits_cache.get(user_email, (None, 0))[0] + async def refresh_space_token(self, refresh_token: str) -> typing.Dict: """Refresh Space access token""" space_config = self._get_space_config() diff --git a/web/src/app/home/components/models-dialog/ModelsDialog.tsx b/web/src/app/home/components/models-dialog/ModelsDialog.tsx index f8dc9ca2..6579ca9c 100644 --- a/web/src/app/home/components/models-dialog/ModelsDialog.tsx +++ b/web/src/app/home/components/models-dialog/ModelsDialog.tsx @@ -9,12 +9,11 @@ import { ChevronRight, Trash2, Settings, - Sparkles, LogIn, Eye, Wrench, } from 'lucide-react'; -import { httpClient } from '@/app/infra/http/HttpClient'; +import { httpClient, systemInfo } from '@/app/infra/http/HttpClient'; import { LLMModel, EmbeddingModel, @@ -46,6 +45,7 @@ import { Badge } from '@/components/ui/badge'; import LLMForm from './component/llm-form/LLMForm'; import EmbeddingForm from './component/embedding-form/EmbeddingForm'; import ProviderForm from './component/provider-form/ProviderForm'; +import langbotIcon from '@/app/assets/langbot-logo.webp'; interface ModelsDialogProps { open: boolean; @@ -62,7 +62,7 @@ export default function ModelsDialog({ const [providers, setProviders] = useState([]); const [accountType, setAccountType] = useState<'local' | 'space'>('local'); - const [spaceBalance] = useState(null); + const [spaceCredits, setSpaceCredits] = useState(null); // Expanded providers and their models const [expandedProviders, setExpandedProviders] = useState>( @@ -103,6 +103,10 @@ export default function ModelsDialog({ try { const userInfo = await httpClient.getUserInfo(); setAccountType(userInfo.account_type); + if (userInfo.account_type === 'space') { + const creditsInfo = await httpClient.getSpaceCredits(); + setSpaceCredits(creditsInfo.credits); + } } catch { setAccountType('local'); } @@ -279,8 +283,12 @@ export default function ModelsDialog({
{isLangBotModels ? ( -
- +
+ LangBot
) : ( )} - {isLangBotModels && accountType === 'space' && ( - - {t('models.balance')}: {spaceBalance ?? '--'} - - )} + {isLangBotModels && + accountType === 'space' && + spaceCredits !== null && ( +
+ + {(spaceCredits / 5000).toFixed(2)} {t('models.credits')} + + +
+ )} {!isLangBotModels && ( <> + ) : ( + spaceCredits !== null && ( +
+ + {(spaceCredits / 5000).toFixed(2)} {t('models.credits')} + + +
+ ) )}
@@ -531,12 +582,15 @@ export default function ModelsDialog({ {t('models.title')} -
+
{/* Fixed LangBot Models Card */}
{renderLangBotModelsCard()}
{/* Add Model Button */} -
+
+ + {t('models.providerCount', { count: otherProviders.length })} +