diff --git a/web/default/src/features/dashboard/components/overview/summary-cards.tsx b/web/default/src/features/dashboard/components/overview/summary-cards.tsx
index c24232561e36..7825f5a5df84 100644
--- a/web/default/src/features/dashboard/components/overview/summary-cards.tsx
+++ b/web/default/src/features/dashboard/components/overview/summary-cards.tsx
@@ -16,11 +16,12 @@ along with this program. If not, see .
For commercial licensing, please contact support@quantumnous.com
*/
-import { useMemo } from 'react'
+import { useEffect, useMemo } from 'react'
import { useQuery } from '@tanstack/react-query'
import { Link } from '@tanstack/react-router'
import { ArrowRight, CreditCard } from 'lucide-react'
import { useTranslation } from 'react-i18next'
+import { toast } from 'sonner'
import { useAuthStore } from '@/stores/auth-store'
import { getCurrencyLabel, isCurrencyDisplayEnabled } from '@/lib/currency'
import { formatNumber, formatQuota } from '@/lib/format'
@@ -87,11 +88,53 @@ function buildSummarySparklines(
}
}
+/** localStorage flag so we only celebrate the first-call moment once
+ * per browser. The user may have triggered their first call from
+ * Cherry Studio / Playground / Cursor / curl — wherever — and we'd
+ * like to acknowledge it the next time they open the dashboard.
+ * Backend doesn't track `first_call_at` so we rely on request_count
+ * becoming non-zero as the signal. */
+const FIRST_CALL_CELEBRATED_KEY = 'dr_first_call_celebrated'
+
+function hasCelebrated(): boolean {
+ if (typeof window === 'undefined') return true
+ try {
+ return window.localStorage.getItem(FIRST_CALL_CELEBRATED_KEY) === '1'
+ } catch {
+ return true
+ }
+}
+
+function markCelebrated(): void {
+ if (typeof window === 'undefined') return
+ try {
+ window.localStorage.setItem(FIRST_CALL_CELEBRATED_KEY, '1')
+ } catch {
+ /* private mode / storage disabled — silent */
+ }
+}
+
export function SummaryCards() {
const { t } = useTranslation()
const user = useAuthStore((state) => state.auth.user)
const { status, loading } = useStatus()
+ // First-call celebration: pure frontend, no backend `first_call_at`
+ // field needed. When request_count flips from 0 → 1+, fire one
+ // sonner toast and set a localStorage flag so it never repeats.
+ useEffect(() => {
+ const count = Number(user?.request_count ?? 0)
+ if (count > 0 && !hasCelebrated()) {
+ toast.success(t('🎉 Your first API call worked!'), {
+ description: t(
+ 'You are officially live on DeepRouter. Welcome aboard.'
+ ),
+ duration: 6000,
+ })
+ markCelebrated()
+ }
+ }, [user?.request_count, t])
+
const summaryTimeRange = useMemo(() => computeTimeRange(1), [])
const usageTrendQuery = useQuery({
@@ -179,10 +222,12 @@ export function SummaryCards() {
- {t('Usage at a glance')}
+ {t('Your AI usage')}
- {t('Monitor balance, usage, and request volume')}
+ {t(
+ 'What you have, what you used, how many calls you made.'
+ )}
@@ -220,11 +265,32 @@ export function SummaryCards() {
aria-hidden='true'
/>
-
- {currencyEnabled
- ? `${t('Displayed in')} ${currencyLabel}`
- : t('Balance is shown in quota units')}
-
+ {/* Friendly "how many chats" estimate using a mid-tier model
+ * average ($0.005/chat). Quota units are 500_000 = $1 so
+ * chats ≈ quota / 2_500. Marketing-grade approximation; the
+ * actual cost depends on which model the user invokes. */}
+ {(() => {
+ const remainQuota = Number(user?.quota ?? 0)
+ if (remainQuota <= 0) {
+ return (
+
+ {t('Top up to start using AI models.')}
+
+ )
+ }
+ const chats = Math.max(0, Math.floor(remainQuota / 2500))
+ const chatsLabel =
+ chats >= 10_000
+ ? `${Math.floor(chats / 1000)}k`
+ : chats >= 1000
+ ? `${(chats / 1000).toFixed(1).replace(/\.0$/, '')}k`
+ : String(chats)
+ return (
+
+ {t('≈ {{count}} chats remaining', { count: chatsLabel })}
+
+ )
+ })()}
}>
{t('Recharge')}
diff --git a/web/default/src/features/pricing/components/model-details.tsx b/web/default/src/features/pricing/components/model-details.tsx
index 2746b221e0f2..68a5e53f83d9 100644
--- a/web/default/src/features/pricing/components/model-details.tsx
+++ b/web/default/src/features/pricing/components/model-details.tsx
@@ -21,6 +21,7 @@ import { useQuery } from '@tanstack/react-query'
import { useNavigate, useParams, useSearch } from '@tanstack/react-router'
import { ArrowLeft, Code2, HeartPulse, Info, Timer } from 'lucide-react'
import { useTranslation } from 'react-i18next'
+import { useIsAdmin } from '@/hooks/use-admin'
import { getLobeIcon } from '@/lib/lobe-icon'
import { cn } from '@/lib/utils'
import { Button } from '@/components/ui/button'
@@ -599,6 +600,11 @@ function GroupPricingSection(props: {
showRechargePrice?: boolean
}) {
const { t } = useTranslation()
+ // Group multiplier ("5x") is an operator concept — non-admin users
+ // see the final per-token price computed from the ratio, but never
+ // the multiplier itself. Same pattern as the Create Key form and
+ // Usage Logs detail dialog gates.
+ const isAdmin = useIsAdmin()
const showRechargePrice = props.showRechargePrice ?? false
const availableGroups = useMemo(
@@ -701,9 +707,11 @@ function GroupPricingSection(props: {
-
- {ratio}x
-
+ {isAdmin && (
+
+ {ratio}x
+
+ )}
@@ -775,7 +783,9 @@ function GroupPricingSection(props: {
{t('Group')}
- {t('Ratio')}
+ {isAdmin && (
+ {t('Ratio')}
+ )}
{isTokenBased ? (
<>
@@ -808,9 +818,11 @@ function GroupPricingSection(props: {
-
- {ratio}x
-
+ {isAdmin && (
+
+ {ratio}x
+
+ )}
{isTokenBased ? (
<>
diff --git a/web/default/src/features/pricing/components/pricing-sidebar.tsx b/web/default/src/features/pricing/components/pricing-sidebar.tsx
index 99b161e94358..58fbaa837f59 100644
--- a/web/default/src/features/pricing/components/pricing-sidebar.tsx
+++ b/web/default/src/features/pricing/components/pricing-sidebar.tsx
@@ -19,6 +19,7 @@ For commercial licensing, please contact support@quantumnous.com
import type { ReactNode } from 'react'
import { ChevronDown, RotateCcw } from 'lucide-react'
import { useTranslation } from 'react-i18next'
+import { useIsAdmin } from '@/hooks/use-admin'
import { getLobeIcon } from '@/lib/lobe-icon'
import { cn } from '@/lib/utils'
import { Badge } from '@/components/ui/badge'
@@ -156,6 +157,9 @@ function FilterSection(props: FilterSectionProps) {
export function PricingSidebar(props: PricingSidebarProps) {
const { t } = useTranslation()
+ // Group ratio suffix ("x1.2") is an operator concept; non-admin
+ // users don't need to see markup multipliers next to group filters.
+ const isAdmin = useIsAdmin()
const quotaTypeLabels = getQuotaTypeLabels(t)
const endpointTypeLabels = getEndpointTypeLabels(t)
@@ -186,7 +190,9 @@ export function PricingSidebar(props: PricingSidebarProps) {
...props.groups.map((group) => ({
value: group,
label: group,
- suffix: formatGroupRatio(props.groupRatios?.[group]),
+ suffix: isAdmin
+ ? formatGroupRatio(props.groupRatios?.[group])
+ : undefined,
})),
]
diff --git a/web/default/src/features/wallet/components/dialogs/billing-history-dialog.tsx b/web/default/src/features/wallet/components/dialogs/billing-history-dialog.tsx
index ac58574b58cd..e3a1bbe995bb 100644
--- a/web/default/src/features/wallet/components/dialogs/billing-history-dialog.tsx
+++ b/web/default/src/features/wallet/components/dialogs/billing-history-dialog.tsx
@@ -172,12 +172,14 @@ export function BillingHistoryDialog({
) : records.length === 0 ? (
- {t('No billing records found')}
+ {keyword
+ ? t('No billing records found')
+ : t("You haven't recharged yet")}
{keyword
? t('Try adjusting your search')
- : t('Your transaction history will appear here')}
+ : t('Once you top up, your records appear here.')}
) : (
diff --git a/web/default/src/i18n/locales/en.json b/web/default/src/i18n/locales/en.json
index cf1ebeaba10c..1f477cd54e7c 100644
--- a/web/default/src/i18n/locales/en.json
+++ b/web/default/src/i18n/locales/en.json
@@ -2542,6 +2542,7 @@
"Old format: Direct override. New format: Supports conditional judgment and custom JSON operations.": "Old format: Direct override. New format: Supports conditional judgment and custom JSON operations.",
"Ollama": "Ollama",
"Ollama Models": "Ollama Models",
+ "Once you top up, your records appear here.": "Once you top up, your records appear here.",
"One API": "One API",
"One IP or CIDR range per line": "One IP or CIDR range per line",
"One IP per line (empty for no restriction)": "One IP per line (empty for no restriction)",
@@ -3855,6 +3856,7 @@
"Top up balance and view billing history.": "Top up balance and view billing history.",
"Top up to call AI models. Your trial credit covers your first few requests so you can try things out.": "Top up to call AI models. Your trial credit covers your first few requests so you can try things out.",
"Top up to start using AI models": "Top up to start using AI models",
+ "Top up to start using AI models.": "Top up to start using AI models.",
"Top vendors": "Top vendors",
"Top {{count}}": "Top {{count}}",
"Top-Up Link": "Top-Up Link",
@@ -4226,6 +4228,7 @@
"Well-Known URL must start with http:// or https://": "Well-Known URL must start with http:// or https://",
"What can I help you with?": "What can I help you with?",
"What would you like to know?": "What would you like to know?",
+ "What you have, what you used, how many calls you made.": "What you have, what you used, how many calls you made.",
"When a token uses the auto group, the system tries groups from top to bottom until it finds an available group.": "When a token uses the auto group, the system tries groups from top to bottom until it finds an available group.",
"When conditions match, the final price is multiplied by X. Multiple matches multiply together; values < 1 act as discounts.": "When conditions match, the final price is multiplied by X. Multiple matches multiply together; values < 1 act as discounts.",
"When enabled, Midjourney callbacks are accepted (reveals server IP).": "When enabled, Midjourney callbacks are accepted (reveals server IP).",
@@ -4261,6 +4264,7 @@
"Year": "Year",
"You Pay": "You Pay",
"You are about to delete {{count}} API key(s).": "You are about to delete {{count}} API key(s).",
+ "You are officially live on DeepRouter. Welcome aboard.": "You are officially live on DeepRouter. Welcome aboard.",
"You are running the latest version ({{version}}).": "You are running the latest version ({{version}}).",
"You can close this tab once the binding completes or a success message appears in the original window.": "You can close this tab once the binding completes or a success message appears in the original window.",
"You can manually add them in \"Custom Model Names\", click \"Fill\" and then submit, or use the operations below to handle automatically.": "You can manually add them in \"Custom Model Names\", click \"Fill\" and then submit, or use the operations below to handle automatically.",
@@ -4268,9 +4272,11 @@
"You don't have necessary permission": "You don't have necessary permission",
"You have unsaved changes": "You have unsaved changes",
"You have unsaved changes. Are you sure you want to leave?": "You have unsaved changes. Are you sure you want to leave?",
+ "You haven't recharged yet": "You haven't recharged yet",
"You save": "You save",
"You will be redirected to Telegram to complete the binding process.": "You will be redirected to Telegram to complete the binding process.",
"You'll be redirected automatically. You can return to the previous page if nothing happens after a few seconds.": "You'll be redirected automatically. You can return to the previous page if nothing happens after a few seconds.",
+ "Your AI usage": "Your AI usage",
"Your Azure OpenAI endpoint URL": "Your Azure OpenAI endpoint URL",
"Your Bot Name": "Your Bot Name",
"Your Cloudflare Account ID": "Your Cloudflare Account ID",
@@ -4529,6 +4535,7 @@
"| Based on": "| Based on",
"© 2025 Your Company. All rights reserved.": "© 2025 Your Company. All rights reserved.",
"≈ {{count}} chats": "≈ {{count}} chats",
- "≈ {{count}} chats remaining": "≈ {{count}} chats remaining"
+ "≈ {{count}} chats remaining": "≈ {{count}} chats remaining",
+ "🎉 Your first API call worked!": "🎉 Your first API call worked!"
}
}
diff --git a/web/default/src/i18n/locales/zh.json b/web/default/src/i18n/locales/zh.json
index 5e143c60bf1e..d4cdee731cc0 100644
--- a/web/default/src/i18n/locales/zh.json
+++ b/web/default/src/i18n/locales/zh.json
@@ -2542,6 +2542,7 @@
"Old format: Direct override. New format: Supports conditional judgment and custom JSON operations.": "旧格式:直接覆盖。新格式:支持条件判断和自定义 JSON 操作。",
"Ollama": "Ollama",
"Ollama Models": "Ollama 模型",
+ "Once you top up, your records appear here.": "充值后记录会出现在这里。",
"One API": "One API",
"One IP or CIDR range per line": "每行一个 IP 或 CIDR 范围",
"One IP per line (empty for no restriction)": "每行一个 IP (留空表示无限制)",
@@ -3855,6 +3856,7 @@
"Top up balance and view billing history.": "充值余额并查看账单历史。",
"Top up to call AI models. Your trial credit covers your first few requests so you can try things out.": "充值后调用 AI 模型。注册赠送的额度够你试用前几次请求。",
"Top up to start using AI models": "充值后开始使用 AI 模型",
+ "Top up to start using AI models.": "充值后开始使用 AI 模型。",
"Top vendors": "热门厂商",
"Top {{count}}": "前 {{count}}",
"Top-Up Link": "充值链接",
@@ -4226,6 +4228,7 @@
"Well-Known URL must start with http:// or https://": "知名 URL 必须以 http:// 或 https:// 开头",
"What can I help you with?": "今天可以帮你做点什么?",
"What would you like to know?": "您想了解什么?",
+ "What you have, what you used, how many calls you made.": "余额、消费、调用次数一览。",
"When a token uses the auto group, the system tries groups from top to bottom until it finds an available group.": "当令牌使用 auto 分组时,系统会按从上到下的顺序尝试,直到找到可用分组。",
"When conditions match, the final price is multiplied by X. Multiple matches multiply together; values < 1 act as discounts.": "条件满足时,最终价格乘以 X;多条命中的倍率会相乘;小于 1 的值为折扣。",
"When enabled, Midjourney callbacks are accepted (reveals server IP).": "启用时,接受 Midjourney 回调 (会泄露服务器 IP)。",
@@ -4261,6 +4264,7 @@
"Year": "今年",
"You Pay": "您支付",
"You are about to delete {{count}} API key(s).": "您即将删除 {{count}} 个 API 密钥。",
+ "You are officially live on DeepRouter. Welcome aboard.": "你正式开始用 DeepRouter 啦,欢迎!",
"You are running the latest version ({{version}}).": "您正在运行最新版本 ({{version}})。",
"You can close this tab once the binding completes or a success message appears in the original window.": "绑定完成后或原窗口出现成功消息后,您可以关闭此标签页。",
"You can manually add them in \"Custom Model Names\", click \"Fill\" and then submit, or use the operations below to handle automatically.": "你可以在\"自定义模型名称\"处手动添加它们,然后点击\"填入\"后再提交,或者直接使用下方操作自动处理。",
@@ -4268,9 +4272,11 @@
"You don't have necessary permission": "您没有必要的权限",
"You have unsaved changes": "您有未保存的更改",
"You have unsaved changes. Are you sure you want to leave?": "您有未保存的更改。确定要离开吗?",
+ "You haven't recharged yet": "你还没有充值过",
"You save": "您节省",
"You will be redirected to Telegram to complete the binding process.": "您将被重定向到 Telegram 以完成绑定过程。",
"You'll be redirected automatically. You can return to the previous page if nothing happens after a few seconds.": "您将被自动重定向。如果几秒钟后无反应,您可以返回上一页。",
+ "Your AI usage": "你的 AI 用量",
"Your Azure OpenAI endpoint URL": "您的 Azure OpenAI 端点 URL",
"Your Bot Name": "您的机器人名称",
"Your Cloudflare Account ID": "您的 Cloudflare 账户 ID",
@@ -4529,6 +4535,7 @@
"| Based on": "| 基于",
"© 2025 Your Company. All rights reserved.": "© 2025 您的公司。保留所有权利。",
"≈ {{count}} chats": "≈ 聊 {{count}} 次",
- "≈ {{count}} chats remaining": "≈ 还能聊 {{count}} 次"
+ "≈ {{count}} chats remaining": "≈ 还能聊 {{count}} 次",
+ "🎉 Your first API call worked!": "🎉 第一次 API 调用成功了!"
}
}