Move plan/quota UI to Account; drop per-message tokens (#73)
Unit Tests / test (pull_request) Successful in 10s

Remove chat dashboard usage bar and assistant token footers. Surface
plan, prompt quota, and period tokens as an Account page card instead.
This commit is contained in:
2026-07-31 07:42:46 -05:00
parent c6bacbb23c
commit 14a2f476f6
6 changed files with 126 additions and 121 deletions
@@ -17,8 +17,6 @@ const theme = {
const renderCard = (props: { const renderCard = (props: {
message: string; message: string;
user_created: boolean; user_created: boolean;
tokens_in?: number | null;
tokens_out?: number | null;
}) => }) =>
render( render(
<ThemeProvider theme={theme as never}> <ThemeProvider theme={theme as never}>
@@ -26,38 +24,23 @@ const renderCard = (props: {
</ThemeProvider> </ThemeProvider>
); );
describe('ConversationDetailCard token usage', () => { describe('ConversationDetailCard', () => {
it('shows em dash when assistant tokens are missing', () => { it('renders assistant message without token meta', () => {
renderCard({ renderCard({
message: 'Hello from the model', message: 'Hello from the model',
user_created: false, user_created: false,
tokens_in: null,
tokens_out: null,
}); });
expect(screen.getByTestId('message-token-usage')).toHaveTextContent( expect(screen.getByText('Hello from the model')).toBeInTheDocument();
'Tokens in — · out —' expect(screen.queryByTestId('message-token-usage')).not.toBeInTheDocument();
); expect(screen.queryByText(/Tokens in/i)).not.toBeInTheDocument();
}); });
it('shows reported token counts for assistant messages', () => { it('renders user message without token meta', () => {
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', () => {
renderCard({ renderCard({
message: 'Hi', message: 'Hi',
user_created: true, user_created: true,
tokens_in: 1,
tokens_out: 2,
}); });
expect(screen.getByText('Hi')).toBeInTheDocument();
expect(screen.queryByTestId('message-token-usage')).not.toBeInTheDocument(); expect(screen.queryByTestId('message-token-usage')).not.toBeInTheDocument();
}); });
}); });
@@ -1,7 +1,6 @@
import React from "react"; import React from "react";
import Markdown from "markdown-to-jsx"; import Markdown from "markdown-to-jsx";
import styled, { keyframes } from "styled-components"; import styled, { keyframes } from "styled-components";
import { formatTokenCount } from "../../utils/finance";
const fadeIn = keyframes` const fadeIn = keyframes`
from { opacity: 0; transform: translateY(10px); } 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` const LoadingDot = styled.div`
width: 8px; width: 8px;
height: 8px; height: 8px;
@@ -104,8 +94,6 @@ const LoadingContainer = styled.div`
type ConversationDetailCardProps = { type ConversationDetailCardProps = {
message: string; message: string;
user_created: boolean; user_created: boolean;
tokens_in?: number | null;
tokens_out?: number | null;
}; };
const MyPlot = ({ format, image }: { format: string; image: string }) => { const MyPlot = ({ format, image }: { format: string; image: string }) => {
@@ -130,8 +118,6 @@ const MyError = ({ content }: { content: string }) => {
const ConversationDetailCard = ({ const ConversationDetailCard = ({
message, message,
user_created, user_created,
tokens_in = null,
tokens_out = null,
}: ConversationDetailCardProps): JSX.Element => { }: ConversationDetailCardProps): JSX.Element => {
if (message.length === 0) { if (message.length === 0) {
return ( return (
@@ -171,8 +157,6 @@ const ConversationDetailCard = ({
} }
} catch { } } catch { }
const showTokens = !user_created;
return ( return (
<MessageContainer $isUser={user_created}> <MessageContainer $isUser={user_created}>
<Bubble $isUser={user_created}> <Bubble $isUser={user_created}>
@@ -193,11 +177,6 @@ const ConversationDetailCard = ({
{contentToAdd} {contentToAdd}
</Markdown> </Markdown>
</Bubble> </Bubble>
{showTokens ? (
<TokenMeta $isUser={user_created} data-testid="message-token-usage">
Tokens in {formatTokenCount(tokens_in)} · out {formatTokenCount(tokens_out)}
</TokenMeta>
) : null}
</MessageContainer> </MessageContainer>
); );
}; };
@@ -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<SubscriptionMe | null>(null);
const load = useCallback(async () => {
try {
const response = await axiosInstance.get<SubscriptionMe>('/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 (
<Bar data-testid="usage-summary-bar">
<Chip>
<strong>{subscription.plan.name}</strong>
</Chip>
<Chip>
Prompts left ({usage.window_hours}h): {promptsLabel}
</Chip>
<Chip>
Period tokens in {formatTokenCount(usage.tokens_in_period)} · out{' '}
{formatTokenCount(usage.tokens_out_period)}
</Chip>
</Bar>
);
};
export default UsageSummaryBar;
@@ -0,0 +1,117 @@
import React, { useCallback, useEffect, useState } from 'react';
import styled from 'styled-components';
import { axiosInstance } from '../../../axiosApi';
import {
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-bottom: 1.5rem;
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};
&:last-child {
border-bottom: none;
}
`;
const SettingLabel = styled.span`
font-size: 1.1rem;
color: ${({ theme }) => theme.colors.text};
font-weight: 500;
`;
const SettingValue = styled.span`
font-size: 1.1rem;
color: ${({ theme }) => theme.colors.text};
opacity: 0.9;
text-align: right;
`;
const BodyText = styled.p`
margin: 0;
color: ${({ theme }) => theme.colors.text};
opacity: 0.75;
line-height: 1.5;
`;
const UsageSummaryCard = (): JSX.Element => {
const [subscription, setSubscription] = useState<SubscriptionMe | null>(null);
const [loading, setLoading] = useState(true);
const load = useCallback(async () => {
setLoading(true);
try {
const response = await axiosInstance.get<SubscriptionMe>('/finance/subscription/');
setSubscription(response.data || null);
} catch {
setSubscription(null);
} finally {
setLoading(false);
}
}, []);
useEffect(() => {
load();
}, [load]);
const usage = subscription?.usage;
const promptsLabel =
!usage || usage.prompts_remaining === null || usage.prompt_quota === null
? '—'
: `${usage.prompts_remaining}/${usage.prompt_quota}`;
return (
<GlassCard data-testid="usage-summary-card">
<CardTitle>Plan &amp; usage</CardTitle>
{loading ? (
<BodyText>Loading usage</BodyText>
) : !subscription?.plan ? (
<BodyText>No active plan yet.</BodyText>
) : (
<>
<SettingRow>
<SettingLabel>Plan</SettingLabel>
<SettingValue>{subscription.plan.name}</SettingValue>
</SettingRow>
<SettingRow>
<SettingLabel>Prompts left ({usage?.window_hours ?? '—'}h)</SettingLabel>
<SettingValue>{promptsLabel}</SettingValue>
</SettingRow>
<SettingRow>
<SettingLabel>Period tokens</SettingLabel>
<SettingValue>
in {formatTokenCount(usage?.tokens_in_period ?? null)} · out{' '}
{formatTokenCount(usage?.tokens_out_period ?? null)}
</SettingValue>
</SettingRow>
</>
)}
</GlassCard>
);
};
export default UsageSummaryCard;
@@ -9,6 +9,7 @@ import Header2 from "../../components/Header2/Header2";
import ParticleBackground from "../../components/ParticleBackground/ParticleBackground"; import ParticleBackground from "../../components/ParticleBackground/ParticleBackground";
import styled from "styled-components"; import styled from "styled-components";
import ThemeSettingsCard from "../../components/ThemeSettingsCard/ThemeSettingsCard"; import ThemeSettingsCard from "../../components/ThemeSettingsCard/ThemeSettingsCard";
import UsageSummaryCard from "../../components/UsageSummaryCard/UsageSummaryCard";
import BillingSection from "../../components/BillingSection/BillingSection"; import BillingSection from "../../components/BillingSection/BillingSection";
// Styled Components // Styled Components
@@ -408,6 +409,7 @@ const AccountPage = (): JSX.Element => {
return ( return (
<> <>
<ThemeSettingsCard /> <ThemeSettingsCard />
<UsageSummaryCard />
<BillingSection /> <BillingSection />
{account?.is_company_manager ? ( {account?.is_company_manager ? (
<CompanyManagerCard /> <CompanyManagerCard />
@@ -16,7 +16,6 @@ import { MessageContext } from "../../contexts/MessageContext";
import ParticleBackground from "../../components/ParticleBackground/ParticleBackground"; import ParticleBackground from "../../components/ParticleBackground/ParticleBackground";
import Header2 from "../../components/Header2/Header2"; import Header2 from "../../components/Header2/Header2";
import UsageSummaryBar from "../../components/UsageSummaryBar/UsageSummaryBar";
import { AnalyticsEvents, trackEvent } from "../../utils/analytics"; import { AnalyticsEvents, trackEvent } from "../../utils/analytics";
// Styled Components // Styled Components
@@ -517,7 +516,6 @@ const AsyncDashboardInner = (): JSX.Element => {
</Sidebar> </Sidebar>
<MainContent> <MainContent>
<UsageSummaryBar />
<ChatArea> <ChatArea>
{conversationDetails.length > 0 ? ( {conversationDetails.length > 0 ? (
conversationDetails.map((convo_detail, index) => conversationDetails.map((convo_detail, index) =>
@@ -525,16 +523,12 @@ const AsyncDashboardInner = (): JSX.Element => {
<ConversationDetailCard <ConversationDetailCard
message={convo_detail.message} message={convo_detail.message}
user_created={convo_detail.user_created} user_created={convo_detail.user_created}
tokens_in={convo_detail.tokens_in}
tokens_out={convo_detail.tokens_out}
key={convo_detail.id || index} key={convo_detail.id || index}
/> />
) : ( ) : (
<ConversationDetailCard <ConversationDetailCard
message={stateMessage} message={stateMessage}
user_created={convo_detail.user_created} user_created={convo_detail.user_created}
tokens_in={convo_detail.tokens_in}
tokens_out={convo_detail.tokens_out}
key={convo_detail.id || index} key={convo_detail.id || index}
/> />
) )