## Summary - Closes [#73](#73) - Remove per-message `Tokens in … · out …` under assistant bubbles - Remove dashboard `UsageSummaryBar` (plan / prompts left / period tokens) - Add Account **Usage limit** card (Gemini-style): plan name, prompt progress meter with rolling-window reset hint, monthly tokens meter when capped (else period in/out), upgrade hint pointing at Billing ## Test plan - [ ] Chat dashboard: no usage bar above messages - [ ] Assistant messages: no token footer - [ ] Account: Usage limit card shows plan + prompt bar (+ token bar when monthly quota set) - [ ] `npm test -- --watchAll=false --testPathPattern=ConversationDetailCard` passes Related follow-up: [#75](#75) upgrade/change/cancel subscription.Reviewed-on: #74
This commit was merged in pull request #74.
This commit is contained in:
+7
-24
@@ -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,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 (
|
||||||
|
<MeterBlock>
|
||||||
|
<MeterHeader>
|
||||||
|
<MeterLabel>{label}</MeterLabel>
|
||||||
|
<MeterMeta>{meta}</MeterMeta>
|
||||||
|
</MeterHeader>
|
||||||
|
<Track aria-hidden={!showBar}>
|
||||||
|
{showBar ? <Fill $pct={pct} $warn={warn} /> : null}
|
||||||
|
</Track>
|
||||||
|
<MeterFooter>
|
||||||
|
<span>{usedLabel}</span>
|
||||||
|
<span>{showBar ? `${pct}%` : '—'}</span>
|
||||||
|
</MeterFooter>
|
||||||
|
</MeterBlock>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
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 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 (
|
||||||
|
<GlassCard data-testid="usage-summary-card">
|
||||||
|
<CardTitle>Usage limit</CardTitle>
|
||||||
|
{loading ? (
|
||||||
|
<BodyText>Loading usage…</BodyText>
|
||||||
|
) : !subscription?.plan ? (
|
||||||
|
<BodyText>No active plan yet.</BodyText>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<PlanName>{subscription.plan.name}</PlanName>
|
||||||
|
|
||||||
|
<UsageMeter
|
||||||
|
label="Prompts"
|
||||||
|
meta={`Resets every ${usage?.window_hours ?? '—'}h`}
|
||||||
|
usedLabel={
|
||||||
|
promptQuota === null
|
||||||
|
? `${promptsUsed.toLocaleString()} used`
|
||||||
|
: `${promptsUsed.toLocaleString()} / ${promptQuota.toLocaleString()}`
|
||||||
|
}
|
||||||
|
pct={promptPct}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{tokenQuota !== null ? (
|
||||||
|
<UsageMeter
|
||||||
|
label="Tokens this month"
|
||||||
|
meta={
|
||||||
|
usage?.period_end
|
||||||
|
? `Resets ${formatBillingDate(usage.period_end)}`
|
||||||
|
: 'Calendar month'
|
||||||
|
}
|
||||||
|
usedLabel={
|
||||||
|
tokensUsed === null
|
||||||
|
? `— / ${tokenQuota.toLocaleString()}`
|
||||||
|
: `${formatTokenCount(tokensUsed)} / ${tokenQuota.toLocaleString()}`
|
||||||
|
}
|
||||||
|
pct={tokenPct}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<UsageMeter
|
||||||
|
label="Tokens this period"
|
||||||
|
meta={
|
||||||
|
usage?.period_end
|
||||||
|
? `Period ends ${formatBillingDate(usage.period_end)}`
|
||||||
|
: 'This month'
|
||||||
|
}
|
||||||
|
usedLabel={`in ${formatTokenCount(usage?.tokens_in_period ?? null)} · out ${formatTokenCount(usage?.tokens_out_period ?? null)}`}
|
||||||
|
pct={null}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<UpgradeHint>
|
||||||
|
Need more capacity? Manage or upgrade your plan in Billing below.
|
||||||
|
</UpgradeHint>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</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}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user