Move plan/quota UI to Account; drop per-message tokens (#73)
Unit Tests / test (pull_request) Successful in 10s
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:
+7
-24
@@ -17,8 +17,6 @@ const theme = {
|
||||
const renderCard = (props: {
|
||||
message: string;
|
||||
user_created: boolean;
|
||||
tokens_in?: number | null;
|
||||
tokens_out?: number | null;
|
||||
}) =>
|
||||
render(
|
||||
<ThemeProvider theme={theme as never}>
|
||||
@@ -26,38 +24,23 @@ const renderCard = (props: {
|
||||
</ThemeProvider>
|
||||
);
|
||||
|
||||
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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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 (
|
||||
<MessageContainer $isUser={user_created}>
|
||||
<Bubble $isUser={user_created}>
|
||||
@@ -193,11 +177,6 @@ const ConversationDetailCard = ({
|
||||
{contentToAdd}
|
||||
</Markdown>
|
||||
</Bubble>
|
||||
{showTokens ? (
|
||||
<TokenMeta $isUser={user_created} data-testid="message-token-usage">
|
||||
Tokens in {formatTokenCount(tokens_in)} · out {formatTokenCount(tokens_out)}
|
||||
</TokenMeta>
|
||||
) : null}
|
||||
</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 & 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 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 (
|
||||
<>
|
||||
<ThemeSettingsCard />
|
||||
<UsageSummaryCard />
|
||||
<BillingSection />
|
||||
{account?.is_company_manager ? (
|
||||
<CompanyManagerCard />
|
||||
|
||||
@@ -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 => {
|
||||
</Sidebar>
|
||||
|
||||
<MainContent>
|
||||
<UsageSummaryBar />
|
||||
<ChatArea>
|
||||
{conversationDetails.length > 0 ? (
|
||||
conversationDetails.map((convo_detail, index) =>
|
||||
@@ -525,16 +523,12 @@ const AsyncDashboardInner = (): JSX.Element => {
|
||||
<ConversationDetailCard
|
||||
message={convo_detail.message}
|
||||
user_created={convo_detail.user_created}
|
||||
tokens_in={convo_detail.tokens_in}
|
||||
tokens_out={convo_detail.tokens_out}
|
||||
key={convo_detail.id || index}
|
||||
/>
|
||||
) : (
|
||||
<ConversationDetailCard
|
||||
message={stateMessage}
|
||||
user_created={convo_detail.user_created}
|
||||
tokens_in={convo_detail.tokens_in}
|
||||
tokens_out={convo_detail.tokens_out}
|
||||
key={convo_detail.id || index}
|
||||
/>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user