From d799379eb724ede36ad3f91dec446f16dd9938d1 Mon Sep 17 00:00:00 2001 From: Ryan Westfall Date: Fri, 31 Jul 2026 09:07:12 -0500 Subject: [PATCH] Restyle Account usage card like Gemini usage limits Show plan name with prompt/token progress meters and reset hints instead of plain key-value rows. --- .../UsageSummaryCard/UsageSummaryCard.tsx | 197 ++++++++++++++---- 1 file changed, 158 insertions(+), 39 deletions(-) diff --git a/llm-fe/src/llm-fe/components/UsageSummaryCard/UsageSummaryCard.tsx b/llm-fe/src/llm-fe/components/UsageSummaryCard/UsageSummaryCard.tsx index 9b7190c..703838c 100644 --- a/llm-fe/src/llm-fe/components/UsageSummaryCard/UsageSummaryCard.tsx +++ b/llm-fe/src/llm-fe/components/UsageSummaryCard/UsageSummaryCard.tsx @@ -2,6 +2,7 @@ import React, { useCallback, useEffect, useState } from 'react'; import styled from 'styled-components'; import { axiosInstance } from '../../../axiosApi'; import { + formatBillingDate, formatTokenCount, SubscriptionMe, } from '../../utils/finance'; @@ -20,35 +21,72 @@ const GlassCard = styled.div` const CardTitle = styled.h2` font-size: 1.8rem; - margin-bottom: 1.5rem; + margin: 0 0 0.35rem 0; color: ${({ theme }) => theme.colors.text}; - border-bottom: 1px solid ${({ theme }) => theme.colors.cardBorder}; - padding-bottom: 1rem; `; -const SettingRow = styled.div` - display: flex; - justify-content: space-between; - align-items: center; - padding: 1rem 0; - border-bottom: 1px solid ${({ theme }) => theme.colors.cardBorder}; +const PlanName = styled.p` + margin: 0 0 1.75rem 0; + font-size: 1rem; + font-weight: 600; + color: ${({ theme }) => theme.colors.text}; + opacity: 0.85; +`; - &:last-child { - border-bottom: none; +const MeterBlock = styled.div` + margin-bottom: 1.5rem; + + &:last-of-type { + margin-bottom: 0; } `; -const SettingLabel = styled.span` - font-size: 1.1rem; - color: ${({ theme }) => theme.colors.text}; - font-weight: 500; +const MeterHeader = styled.div` + display: flex; + justify-content: space-between; + align-items: baseline; + gap: 1rem; + margin-bottom: 0.55rem; `; -const SettingValue = styled.span` - font-size: 1.1rem; +const MeterLabel = styled.span` + font-size: 1rem; + font-weight: 600; color: ${({ theme }) => theme.colors.text}; - opacity: 0.9; - text-align: right; +`; + +const MeterMeta = styled.span` + font-size: 0.85rem; + color: ${({ theme }) => theme.colors.text}; + opacity: 0.65; + white-space: nowrap; +`; + +const Track = styled.div` + width: 100%; + height: 0.55rem; + border-radius: 999px; + background: ${({ theme }) => + theme.darkMode ? 'rgba(255, 255, 255, 0.12)' : 'rgba(0, 0, 0, 0.1)'}; + overflow: hidden; +`; + +const Fill = styled.div<{ $pct: number; $warn: boolean }>` + height: 100%; + width: ${({ $pct }) => `${Math.min(100, Math.max(0, $pct))}%`}; + border-radius: 999px; + background: ${({ theme, $warn }) => ($warn ? '#e67e22' : theme.main)}; + transition: width 0.35s ease; +`; + +const MeterFooter = styled.div` + display: flex; + justify-content: space-between; + gap: 1rem; + margin-top: 0.4rem; + font-size: 0.8rem; + color: ${({ theme }) => theme.colors.text}; + opacity: 0.65; `; const BodyText = styled.p` @@ -58,6 +96,49 @@ const BodyText = styled.p` line-height: 1.5; `; +const UpgradeHint = styled.p` + margin: 1.5rem 0 0 0; + padding-top: 1.25rem; + border-top: 1px solid ${({ theme }) => theme.colors.cardBorder}; + font-size: 0.9rem; + color: ${({ theme }) => theme.colors.text}; + opacity: 0.7; + line-height: 1.45; +`; + +function clampPct(used: number, quota: number): number { + if (quota <= 0) return 0; + return Math.min(100, Math.round((used / quota) * 1000) / 10); +} + +type MeterProps = { + label: string; + meta: string; + usedLabel: string; + pct: number | null; +}; + +const UsageMeter = ({ label, meta, usedLabel, pct }: MeterProps): JSX.Element => { + const showBar = pct !== null; + const warn = showBar && pct >= 90; + + return ( + + + {label} + {meta} + + + {showBar ? : null} + + + {usedLabel} + {showBar ? `${pct}%` : '—'} + + + ); +}; + const UsageSummaryCard = (): JSX.Element => { const [subscription, setSubscription] = useState(null); const [loading, setLoading] = useState(true); @@ -79,35 +160,73 @@ const UsageSummaryCard = (): JSX.Element => { }, [load]); const usage = subscription?.usage; - const promptsLabel = - !usage || usage.prompts_remaining === null || usage.prompt_quota === null - ? '—' - : `${usage.prompts_remaining}/${usage.prompt_quota}`; + const promptQuota = usage?.prompt_quota ?? null; + const promptsUsed = usage?.prompts_in_window ?? 0; + const promptPct = + promptQuota !== null ? clampPct(promptsUsed, promptQuota) : null; + + const tokenQuota = usage?.monthly_token_quota ?? null; + const tokensUsed = usage?.tokens_total_period ?? null; + const tokenPct = + tokenQuota !== null && tokensUsed !== null + ? clampPct(tokensUsed, tokenQuota) + : tokenQuota !== null + ? 0 + : null; return ( - Plan & usage + Usage limit {loading ? ( Loading usage… ) : !subscription?.plan ? ( No active plan yet. ) : ( <> - - Plan - {subscription.plan.name} - - - Prompts left ({usage?.window_hours ?? '—'}h) - {promptsLabel} - - - Period tokens - - in {formatTokenCount(usage?.tokens_in_period ?? null)} · out{' '} - {formatTokenCount(usage?.tokens_out_period ?? null)} - - + {subscription.plan.name} + + + + {tokenQuota !== null ? ( + + ) : ( + + )} + + + Need more capacity? Manage or upgrade your plan in Billing below. + )}