diff --git a/app/src/components/chat/TokenUsagePill.tsx b/app/src/components/chat/TokenUsagePill.tsx new file mode 100644 index 000000000..50badfbeb --- /dev/null +++ b/app/src/components/chat/TokenUsagePill.tsx @@ -0,0 +1,95 @@ +import { useNavigate } from 'react-router-dom'; + +import { useUsageState } from '../../hooks/useUsageState'; +import { useAppSelector } from '../../store/hooks'; + +function formatTokens(n: number): string { + if (n < 1000) return String(n); + if (n < 1_000_000) return `${(n / 1000).toFixed(n < 10_000 ? 1 : 0)}K`; + return `${(n / 1_000_000).toFixed(1)}M`; +} + +interface PillSeverity { + bg: string; + text: string; + ring: string; + label: string; +} + +function severityFromPct(pct: number): PillSeverity { + if (pct >= 0.9) { + return { + bg: 'bg-coral-50', + text: 'text-coral-700', + ring: 'ring-coral-200', + label: `${Math.round(pct * 100)}%`, + }; + } + if (pct >= 0.7) { + return { + bg: 'bg-amber-50', + text: 'text-amber-700', + ring: 'ring-amber-200', + label: `${Math.round(pct * 100)}%`, + }; + } + return { + bg: 'bg-sage-50', + text: 'text-sage-700', + ring: 'ring-sage-200', + label: `${Math.round(pct * 100)}%`, + }; +} + +const TokenUsagePill = () => { + const navigate = useNavigate(); + const sessionTokens = useAppSelector(state => state.chatRuntime.sessionTokenUsage); + const { usagePct10h, usagePct7d, isAtLimit, isNearLimit, currentTier, teamUsage } = + useUsageState(); + + const totalTokens = sessionTokens.inputTokens + sessionTokens.outputTokens; + const showSessionCounter = totalTokens > 0; + + const planPct = Math.max(usagePct10h, usagePct7d); + const planSeverity = severityFromPct(planPct); + const showPlanPill = teamUsage !== null; + + const planTitle = (() => { + if (isAtLimit) return 'Usage limit reached — click to top up'; + if (isNearLimit) return 'Approaching usage limit'; + return `${currentTier.toLowerCase()} plan — click for details`; + })(); + + if (!showSessionCounter && !showPlanPill) return null; + + return ( +
+ {showSessionCounter ? ( + + + + + {formatTokens(totalTokens)} + + ) : null} + {showPlanPill ? ( + + ) : null} +
+ ); +}; + +export default TokenUsagePill; diff --git a/app/src/pages/Conversations.tsx b/app/src/pages/Conversations.tsx index 850c922d4..ef625f896 100644 --- a/app/src/pages/Conversations.tsx +++ b/app/src/pages/Conversations.tsx @@ -4,6 +4,7 @@ import Markdown from 'react-markdown'; import { useNavigate } from 'react-router-dom'; import { type ChatSendError, chatSendError } from '../chat/chatSendError'; +import TokenUsagePill from '../components/chat/TokenUsagePill'; import UpsellBanner from '../components/upsell/UpsellBanner'; import { dismissBanner, shouldShowBanner } from '../components/upsell/upsellDismissState'; import UsageLimitModal from '../components/upsell/UsageLimitModal'; @@ -1114,6 +1115,7 @@ const Conversations = ({ variant = 'page' }: ConversationsProps = {}) => {

{threads.find(t => t.id === selectedThreadId)?.title ?? 'Select a thread'}

+