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 })} +

+ ) + })()}