import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { t } from '@lingui/core/macro';
import { useLingui } from '@lingui/react/macro';
import { useState } from 'react';
import { useRecoilValue } from 'recoil';
import { isDefined } from 'twenty-shared/utils';
import { ProgressBar } from 'twenty-ui/feedback';
import { ContextUsageProgressRing } from '@/ai/components/internal/ContextUsageProgressRing';
import {
agentChatUsageState,
type AgentChatLastMessageUsage,
} from '@/ai/states/agentChatUsageState';
const StyledContainer = styled.div`
position: relative;
`;
const StyledTrigger = styled.div<{ hasUsage: boolean }>`
align-items: center;
background: transparent;
border: 1px solid ${({ theme }) => theme.background.transparent.medium};
border-radius: ${({ theme }) => theme.border.radius.sm};
cursor: ${({ hasUsage }) => (hasUsage ? 'pointer' : 'default')};
display: flex;
gap: ${({ theme }) => theme.spacing(1)};
height: 24px;
padding: 0 ${({ theme }) => theme.spacing(2)};
transition: background 0.1s ease;
&:hover {
background: ${({ theme, hasUsage }) =>
hasUsage ? theme.background.transparent.light : 'transparent'};
}
`;
const StyledPercentage = styled.span`
color: ${({ theme }) => theme.font.color.secondary};
font-size: ${({ theme }) => theme.font.size.sm};
font-weight: ${({ theme }) => theme.font.weight.medium};
`;
const StyledHoverCard = styled.div`
background: ${({ theme }) => theme.background.primary};
border: 1px solid ${({ theme }) => theme.border.color.medium};
border-radius: ${({ theme }) => theme.border.radius.md};
box-shadow: ${({ theme }) => theme.boxShadow.strong};
min-width: 280px;
position: absolute;
right: 0;
bottom: calc(100% + 8px);
z-index: ${({ theme }) => theme.lastLayerZIndex};
`;
const StyledSection = styled.div`
display: flex;
flex-direction: column;
gap: ${({ theme }) => theme.spacing(2)};
padding: ${({ theme }) => theme.spacing(3)};
`;
const StyledRow = styled.div`
align-items: center;
display: flex;
justify-content: space-between;
`;
const StyledLabel = styled.span`
color: ${({ theme }) => theme.font.color.secondary};
font-size: ${({ theme }) => theme.font.size.sm};
`;
const StyledValue = styled.span`
color: ${({ theme }) => theme.font.color.tertiary};
font-size: ${({ theme }) => theme.font.size.sm};
`;
const StyledSectionTitle = styled.span`
color: ${({ theme }) => theme.font.color.primary};
font-size: ${({ theme }) => theme.font.size.xs};
font-weight: ${({ theme }) => theme.font.weight.semiBold};
text-transform: uppercase;
letter-spacing: 0.5px;
`;
const StyledDivider = styled.div`
border-top: 1px solid ${({ theme }) => theme.border.color.light};
`;
const formatTokenCount = (count: number): string => {
if (count >= 1_000_000_000) {
return `${(count / 1_000_000_000).toFixed(1)}B`;
}
if (count >= 1_000_000) {
return `${(count / 1_000_000).toFixed(1)}M`;
}
if (count >= 1_000) {
return `${(count / 1_000).toFixed(1)}K`;
}
return count.toString();
};
const formatCredits = (credits: number): string => {
// Credits are already in display units from the API (internal / 1000)
// Show up to 1 decimal for fractional values, none for whole numbers
if (Number.isInteger(credits)) {
return credits.toLocaleString();
}
return credits.toLocaleString(undefined, {
minimumFractionDigits: 0,
maximumFractionDigits: 1,
});
};
const getCachedLabel = (lastMessage: AgentChatLastMessageUsage): string => {
if (lastMessage.cachedInputTokens <= 0 || lastMessage.inputTokens <= 0) {
return '';
}
const cachedPercent = Math.round(
(lastMessage.cachedInputTokens / lastMessage.inputTokens) * 100,
);
return ` (${t`${cachedPercent}% cached`})`;
};
export const AIChatContextUsageButton = () => {
const { t } = useLingui();
const theme = useTheme();
const [isHovered, setIsHovered] = useState(false);
const agentChatUsage = useRecoilValue(agentChatUsageState);
if (!agentChatUsage) {
return (
0%
);
}
const percentage = Math.min(
(agentChatUsage.conversationSize / agentChatUsage.contextWindowTokens) *
100,
100,
);
const formattedPercentage = percentage.toFixed(1);
const totalCredits =
agentChatUsage.inputCredits + agentChatUsage.outputCredits;
const lastMessage = agentChatUsage.lastMessage;
return (
setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
>
{formattedPercentage}%
{isHovered && (
{formattedPercentage}%
{formatTokenCount(agentChatUsage.conversationSize)} /{' '}
{formatTokenCount(agentChatUsage.contextWindowTokens)}{' '}
{t`tokens`}
80
? theme.color.red
: percentage > 60
? theme.color.orange
: theme.color.blue
}
backgroundColor={theme.background.quaternary}
withBorderRadius
/>
{isDefined(lastMessage) && (
<>
{t`Last message`}
{t`Input tokens`}
{formatTokenCount(lastMessage.inputTokens)}
{getCachedLabel(lastMessage)}
{t`Output tokens`}
{formatTokenCount(lastMessage.outputTokens)}
{t`Cost`}
{formatCredits(
lastMessage.inputCredits + lastMessage.outputCredits,
)}{' '}
{t`credits`}
>
)}
{t`Conversation`}
{t`Input tokens`}
{formatTokenCount(agentChatUsage.inputTokens)}
{t`Output tokens`}
{formatTokenCount(agentChatUsage.outputTokens)}
{t`Total cost`}
{formatCredits(totalCredits)} {t`credits`}
)}
);
};