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