diff --git a/llm-fe/src/llm-fe/components/ConversationDetailCard/ConversationDetailCard.test.tsx b/llm-fe/src/llm-fe/components/ConversationDetailCard/ConversationDetailCard.test.tsx
index 822fa65..7c76895 100644
--- a/llm-fe/src/llm-fe/components/ConversationDetailCard/ConversationDetailCard.test.tsx
+++ b/llm-fe/src/llm-fe/components/ConversationDetailCard/ConversationDetailCard.test.tsx
@@ -17,8 +17,6 @@ const theme = {
const renderCard = (props: {
message: string;
user_created: boolean;
- tokens_in?: number | null;
- tokens_out?: number | null;
}) =>
render(
@@ -26,38 +24,23 @@ const renderCard = (props: {
);
-describe('ConversationDetailCard token usage', () => {
- it('shows em dash when assistant tokens are missing', () => {
+describe('ConversationDetailCard', () => {
+ it('renders assistant message without token meta', () => {
renderCard({
message: 'Hello from the model',
user_created: false,
- tokens_in: null,
- tokens_out: null,
});
- expect(screen.getByTestId('message-token-usage')).toHaveTextContent(
- 'Tokens in — · out —'
- );
+ expect(screen.getByText('Hello from the model')).toBeInTheDocument();
+ expect(screen.queryByTestId('message-token-usage')).not.toBeInTheDocument();
+ expect(screen.queryByText(/Tokens in/i)).not.toBeInTheDocument();
});
- it('shows reported token counts for assistant messages', () => {
- renderCard({
- message: 'Hello from the model',
- user_created: false,
- tokens_in: 12,
- tokens_out: 34,
- });
- expect(screen.getByTestId('message-token-usage')).toHaveTextContent(
- 'Tokens in 12 · out 34'
- );
- });
-
- it('hides token meta on user messages', () => {
+ it('renders user message without token meta', () => {
renderCard({
message: 'Hi',
user_created: true,
- tokens_in: 1,
- tokens_out: 2,
});
+ expect(screen.getByText('Hi')).toBeInTheDocument();
expect(screen.queryByTestId('message-token-usage')).not.toBeInTheDocument();
});
});
diff --git a/llm-fe/src/llm-fe/components/ConversationDetailCard/ConversationDetailCard.tsx b/llm-fe/src/llm-fe/components/ConversationDetailCard/ConversationDetailCard.tsx
index 7129ab9..fabe98c 100644
--- a/llm-fe/src/llm-fe/components/ConversationDetailCard/ConversationDetailCard.tsx
+++ b/llm-fe/src/llm-fe/components/ConversationDetailCard/ConversationDetailCard.tsx
@@ -1,7 +1,6 @@
import React from "react";
import Markdown from "markdown-to-jsx";
import styled, { keyframes } from "styled-components";
-import { formatTokenCount } from "../../utils/finance";
const fadeIn = keyframes`
from { opacity: 0; transform: translateY(10px); }
@@ -68,15 +67,6 @@ const Bubble = styled.div<{ $isUser: boolean }>`
}
`;
-const TokenMeta = styled.div<{ $isUser: boolean }>`
- margin-top: 0.35rem;
- max-width: 80%;
- font-size: 0.75rem;
- opacity: 0.65;
- color: ${({ theme }) => theme.colors.text};
- text-align: ${(props) => (props.$isUser ? "right" : "left")};
-`;
-
const LoadingDot = styled.div`
width: 8px;
height: 8px;
@@ -104,8 +94,6 @@ const LoadingContainer = styled.div`
type ConversationDetailCardProps = {
message: string;
user_created: boolean;
- tokens_in?: number | null;
- tokens_out?: number | null;
};
const MyPlot = ({ format, image }: { format: string; image: string }) => {
@@ -130,8 +118,6 @@ const MyError = ({ content }: { content: string }) => {
const ConversationDetailCard = ({
message,
user_created,
- tokens_in = null,
- tokens_out = null,
}: ConversationDetailCardProps): JSX.Element => {
if (message.length === 0) {
return (
@@ -171,8 +157,6 @@ const ConversationDetailCard = ({
}
} catch { }
- const showTokens = !user_created;
-
return (
@@ -193,11 +177,6 @@ const ConversationDetailCard = ({
{contentToAdd}
- {showTokens ? (
-
- Tokens in {formatTokenCount(tokens_in)} · out {formatTokenCount(tokens_out)}
-
- ) : null}
);
};
diff --git a/llm-fe/src/llm-fe/components/UsageSummaryBar/UsageSummaryBar.tsx b/llm-fe/src/llm-fe/components/UsageSummaryBar/UsageSummaryBar.tsx
deleted file mode 100644
index a97d45c..0000000
--- a/llm-fe/src/llm-fe/components/UsageSummaryBar/UsageSummaryBar.tsx
+++ /dev/null
@@ -1,70 +0,0 @@
-import React, { useCallback, useEffect, useState } from 'react';
-import styled from 'styled-components';
-import { axiosInstance } from '../../../axiosApi';
-import {
- formatTokenCount,
- SubscriptionMe,
-} from '../../utils/finance';
-
-const Bar = styled.div`
- display: flex;
- flex-wrap: wrap;
- gap: 0.75rem 1.25rem;
- align-items: center;
- padding: 0.65rem 1rem;
- margin-bottom: 0.75rem;
- border-radius: 0.75rem;
- background: ${({ theme }) => theme.colors.cardBackground};
- border: 1px solid ${({ theme }) => theme.colors.cardBorder};
- color: ${({ theme }) => theme.colors.text};
- font-size: 0.85rem;
- opacity: 0.95;
-`;
-
-const Chip = styled.span`
- white-space: nowrap;
-`;
-
-const UsageSummaryBar = (): JSX.Element | null => {
- const [subscription, setSubscription] = useState(null);
-
- const load = useCallback(async () => {
- try {
- const response = await axiosInstance.get('/finance/subscription/');
- setSubscription(response.data || null);
- } catch {
- setSubscription(null);
- }
- }, []);
-
- useEffect(() => {
- load();
- }, [load]);
-
- if (!subscription?.plan) {
- return null;
- }
-
- const usage = subscription.usage;
- const promptsLabel =
- usage.prompts_remaining === null || usage.prompt_quota === null
- ? '—'
- : `${usage.prompts_remaining}/${usage.prompt_quota}`;
-
- return (
-
-
- {subscription.plan.name}
-
-
- Prompts left ({usage.window_hours}h): {promptsLabel}
-
-
- Period tokens in {formatTokenCount(usage.tokens_in_period)} · out{' '}
- {formatTokenCount(usage.tokens_out_period)}
-
-
- );
-};
-
-export default UsageSummaryBar;
diff --git a/llm-fe/src/llm-fe/components/UsageSummaryCard/UsageSummaryCard.tsx b/llm-fe/src/llm-fe/components/UsageSummaryCard/UsageSummaryCard.tsx
new file mode 100644
index 0000000..703838c
--- /dev/null
+++ b/llm-fe/src/llm-fe/components/UsageSummaryCard/UsageSummaryCard.tsx
@@ -0,0 +1,236 @@
+import React, { useCallback, useEffect, useState } from 'react';
+import styled from 'styled-components';
+import { axiosInstance } from '../../../axiosApi';
+import {
+ formatBillingDate,
+ formatTokenCount,
+ SubscriptionMe,
+} from '../../utils/finance';
+
+const GlassCard = styled.div`
+ background: ${({ theme }) => theme.colors.cardBackground};
+ backdrop-filter: blur(10px);
+ border: 1px solid ${({ theme }) => theme.colors.cardBorder};
+ border-radius: 1rem;
+ padding: 2rem;
+ width: 100%;
+ max-width: 1000px;
+ margin-bottom: 2rem;
+ box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.37);
+`;
+
+const CardTitle = styled.h2`
+ font-size: 1.8rem;
+ margin: 0 0 0.35rem 0;
+ color: ${({ theme }) => theme.colors.text};
+`;
+
+const PlanName = styled.p`
+ margin: 0 0 1.75rem 0;
+ font-size: 1rem;
+ font-weight: 600;
+ color: ${({ theme }) => theme.colors.text};
+ opacity: 0.85;
+`;
+
+const MeterBlock = styled.div`
+ margin-bottom: 1.5rem;
+
+ &:last-of-type {
+ margin-bottom: 0;
+ }
+`;
+
+const MeterHeader = styled.div`
+ display: flex;
+ justify-content: space-between;
+ align-items: baseline;
+ gap: 1rem;
+ margin-bottom: 0.55rem;
+`;
+
+const MeterLabel = styled.span`
+ font-size: 1rem;
+ font-weight: 600;
+ color: ${({ theme }) => theme.colors.text};
+`;
+
+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`
+ margin: 0;
+ color: ${({ theme }) => theme.colors.text};
+ opacity: 0.75;
+ 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);
+
+ const load = useCallback(async () => {
+ setLoading(true);
+ try {
+ const response = await axiosInstance.get('/finance/subscription/');
+ setSubscription(response.data || null);
+ } catch {
+ setSubscription(null);
+ } finally {
+ setLoading(false);
+ }
+ }, []);
+
+ useEffect(() => {
+ load();
+ }, [load]);
+
+ const usage = subscription?.usage;
+ 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 (
+
+ Usage limit
+ {loading ? (
+ Loading usage…
+ ) : !subscription?.plan ? (
+ No active plan yet.
+ ) : (
+ <>
+ {subscription.plan.name}
+
+
+
+ {tokenQuota !== null ? (
+
+ ) : (
+
+ )}
+
+
+ Need more capacity? Manage or upgrade your plan in Billing below.
+
+ >
+ )}
+
+ );
+};
+
+export default UsageSummaryCard;
diff --git a/llm-fe/src/llm-fe/pages/Account2/Account2.tsx b/llm-fe/src/llm-fe/pages/Account2/Account2.tsx
index 585d82f..790b418 100644
--- a/llm-fe/src/llm-fe/pages/Account2/Account2.tsx
+++ b/llm-fe/src/llm-fe/pages/Account2/Account2.tsx
@@ -9,6 +9,7 @@ import Header2 from "../../components/Header2/Header2";
import ParticleBackground from "../../components/ParticleBackground/ParticleBackground";
import styled from "styled-components";
import ThemeSettingsCard from "../../components/ThemeSettingsCard/ThemeSettingsCard";
+import UsageSummaryCard from "../../components/UsageSummaryCard/UsageSummaryCard";
import BillingSection from "../../components/BillingSection/BillingSection";
// Styled Components
@@ -408,6 +409,7 @@ const AccountPage = (): JSX.Element => {
return (
<>
+
{account?.is_company_manager ? (
diff --git a/llm-fe/src/llm-fe/pages/AsyncDashboard2/AsyncDashboard2.tsx b/llm-fe/src/llm-fe/pages/AsyncDashboard2/AsyncDashboard2.tsx
index 09116fb..1375940 100644
--- a/llm-fe/src/llm-fe/pages/AsyncDashboard2/AsyncDashboard2.tsx
+++ b/llm-fe/src/llm-fe/pages/AsyncDashboard2/AsyncDashboard2.tsx
@@ -16,7 +16,6 @@ import { MessageContext } from "../../contexts/MessageContext";
import ParticleBackground from "../../components/ParticleBackground/ParticleBackground";
import Header2 from "../../components/Header2/Header2";
-import UsageSummaryBar from "../../components/UsageSummaryBar/UsageSummaryBar";
import { AnalyticsEvents, trackEvent } from "../../utils/analytics";
// Styled Components
@@ -517,7 +516,6 @@ const AsyncDashboardInner = (): JSX.Element => {
-
{conversationDetails.length > 0 ? (
conversationDetails.map((convo_detail, index) =>
@@ -525,16 +523,12 @@ const AsyncDashboardInner = (): JSX.Element => {
) : (
)