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`} )} ); };