From 0f0aef72455b0d24025ca79a78f16815c0462909 Mon Sep 17 00:00:00 2001 From: Zane <75694352+zanesq@users.noreply.github.com> Date: Wed, 14 May 2025 16:22:50 -0700 Subject: [PATCH] Added progress info alert for displaying token usage (#2540) --- ui/desktop/src/components/alerts/AlertBox.tsx | 84 ++++++++++++++----- ui/desktop/src/components/alerts/types.ts | 5 ++ ui/desktop/src/components/alerts/useAlerts.ts | 16 +--- .../src/components/bottom_menu/BottomMenu.tsx | 33 ++++++-- .../bottom_menu/BottomMenuAlertPopover.tsx | 34 ++++---- 5 files changed, 117 insertions(+), 55 deletions(-) diff --git a/ui/desktop/src/components/alerts/AlertBox.tsx b/ui/desktop/src/components/alerts/AlertBox.tsx index 6806e4d14a..fa89a5f083 100644 --- a/ui/desktop/src/components/alerts/AlertBox.tsx +++ b/ui/desktop/src/components/alerts/AlertBox.tsx @@ -1,11 +1,12 @@ import React from 'react'; -import { IoIosCloseCircle, IoIosWarning } from 'react-icons/io'; +import { IoIosCloseCircle, IoIosWarning, IoIosInformationCircle } from 'react-icons/io'; import { cn } from '../../utils'; import { Alert, AlertType } from './types'; const alertIcons: Record = { [AlertType.Error]: , [AlertType.Warning]: , + [AlertType.Info]: , }; interface AlertBoxProps { @@ -16,28 +17,73 @@ interface AlertBoxProps { const alertStyles: Record = { [AlertType.Error]: 'bg-[#d7040e] text-white', [AlertType.Warning]: 'bg-[#cc4b03] text-white', + [AlertType.Info]: 'dark:bg-white dark:text-black bg-black text-white', }; export const AlertBox = ({ alert, className }: AlertBoxProps) => { return ( -
-
{alertIcons[alert.type]}
- +
+ {alert.progress ? ( +
+ {alert.message} +
+ {[...Array(30)].map((_, i) => ( +
+ ))} +
+
+
+ + {alert.progress.current >= 1000 + ? (alert.progress.current / 1000).toFixed(1) + 'k' + : alert.progress.current} + + + {Math.round((alert.progress.current / alert.progress.total) * 100)}% + +
+ + {alert.progress.total >= 1000 + ? (alert.progress.total / 1000).toFixed(0) + 'k' + : alert.progress.total} + +
+
+ ) : ( + <> + + + )}
); }; diff --git a/ui/desktop/src/components/alerts/types.ts b/ui/desktop/src/components/alerts/types.ts index c76831cc03..661191e50b 100644 --- a/ui/desktop/src/components/alerts/types.ts +++ b/ui/desktop/src/components/alerts/types.ts @@ -1,6 +1,7 @@ export enum AlertType { Error = 'error', Warning = 'warning', + Info = 'info', } export interface Alert { @@ -11,4 +12,8 @@ export interface Alert { text: string; onClick: () => void; }; + progress?: { + current: number; + total: number; + }; } diff --git a/ui/desktop/src/components/alerts/useAlerts.ts b/ui/desktop/src/components/alerts/useAlerts.ts index bd20a89f1d..8e961aace6 100644 --- a/ui/desktop/src/components/alerts/useAlerts.ts +++ b/ui/desktop/src/components/alerts/useAlerts.ts @@ -1,19 +1,9 @@ import { useState, useCallback } from 'react'; -import { Alert, AlertType } from './types'; - -interface AlertOptions { - type: AlertType; - message: string; - action?: { - text: string; - onClick: () => void; - }; - autoShow?: boolean; -} +import { Alert } from './types'; interface UseAlerts { alerts: Alert[]; - addAlert: (options: AlertOptions) => void; + addAlert: (options: Alert) => void; removeAlert: (index: number) => void; clearAlerts: () => void; } @@ -21,7 +11,7 @@ interface UseAlerts { export const useAlerts = (): UseAlerts => { const [alerts, setAlerts] = useState([]); - const addAlert = useCallback((options: AlertOptions) => { + const addAlert = useCallback((options: Alert) => { setAlerts((prev) => [...prev, options]); }, []); diff --git a/ui/desktop/src/components/bottom_menu/BottomMenu.tsx b/ui/desktop/src/components/bottom_menu/BottomMenu.tsx index a9466c3c62..43c4714abf 100644 --- a/ui/desktop/src/components/bottom_menu/BottomMenu.tsx +++ b/ui/desktop/src/components/bottom_menu/BottomMenu.tsx @@ -40,6 +40,7 @@ export default function BottomMenu({ const toolCount = useToolCount(); const { getProviders, read } = useConfig(); const [tokenLimit, setTokenLimit] = useState(TOKEN_LIMIT_DEFAULT); + const [isTokenLimitLoaded, setIsTokenLimitLoaded] = useState(false); // Load model limits from the API const getModelLimits = async () => { @@ -67,10 +68,14 @@ export default function BottomMenu({ // Load providers and get current model's token limit const loadProviderDetails = async () => { try { + // Reset token limit loaded state + setIsTokenLimitLoaded(false); + // Get current model and provider first to avoid unnecessary provider fetches const { model, provider } = await getCurrentModelAndProvider({ readFromConfig: read }); if (!model || !provider) { console.log('No model or provider found'); + setIsTokenLimitLoaded(true); return; } @@ -83,6 +88,7 @@ export default function BottomMenu({ const modelConfig = currentProvider.metadata.known_models.find((m) => m.name === model); if (modelConfig?.context_limit) { setTokenLimit(modelConfig.context_limit); + setIsTokenLimitLoaded(true); return; } } @@ -92,15 +98,18 @@ export default function BottomMenu({ const fallbackLimit = findModelLimit(model as string, modelLimit); if (fallbackLimit !== null) { setTokenLimit(fallbackLimit); + setIsTokenLimitLoaded(true); return; } // If no match found, use the default model limit setTokenLimit(TOKEN_LIMIT_DEFAULT); + setIsTokenLimitLoaded(true); } catch (err) { console.error('Error loading providers or token limit:', err); // Set default limit on error setTokenLimit(TOKEN_LIMIT_DEFAULT); + setIsTokenLimitLoaded(true); } }; @@ -110,24 +119,36 @@ export default function BottomMenu({ // eslint-disable-next-line react-hooks/exhaustive-deps }, [currentModel]); - // Handle tool count alerts + // Handle tool count alerts and token usage useEffect(() => { clearAlerts(); - // Add token alerts if we have a token limit - if (tokenLimit && numTokens > 0) { + // Only show token alerts if we have loaded the real token limit + if (isTokenLimitLoaded && tokenLimit && numTokens > 0) { if (numTokens >= tokenLimit) { + // Only show error alert when limit reached addAlert({ type: AlertType.Error, - message: `Token limit reached (${numTokens.toLocaleString()}/${tokenLimit.toLocaleString()}) \n You’ve reached the model’s conversation limit. The session will be saved — copy anything important and start a new one to continue.`, + message: `Token limit reached (${numTokens.toLocaleString()}/${tokenLimit.toLocaleString()}) \n You've reached the model's conversation limit. The session will be saved — copy anything important and start a new one to continue.`, autoShow: true, // Auto-show token limit errors }); } else if (numTokens >= tokenLimit * TOKEN_WARNING_THRESHOLD) { + // Only show warning alert when approaching limit addAlert({ type: AlertType.Warning, - message: `Approaching token limit (${numTokens.toLocaleString()}/${tokenLimit.toLocaleString()}) \n You’re reaching the model’s conversation limit. The session will be saved — copy anything important and start a new one to continue.`, + message: `Approaching token limit (${numTokens.toLocaleString()}/${tokenLimit.toLocaleString()}) \n You're reaching the model's conversation limit. The session will be saved — copy anything important and start a new one to continue.`, autoShow: true, // Auto-show token limit warnings }); + } else { + // Show info alert only when not in warning/error state + addAlert({ + type: AlertType.Info, + message: 'Context window', + progress: { + current: numTokens, + total: tokenLimit, + }, + }); } } @@ -145,7 +166,7 @@ export default function BottomMenu({ } // We intentionally omit setView as it shouldn't trigger a re-render of alerts // eslint-disable-next-line react-hooks/exhaustive-deps - }, [numTokens, toolCount, tokenLimit, addAlert, clearAlerts]); + }, [numTokens, toolCount, tokenLimit, isTokenLimitLoaded, addAlert, clearAlerts]); // Add effect to handle clicks outside useEffect(() => { diff --git a/ui/desktop/src/components/bottom_menu/BottomMenuAlertPopover.tsx b/ui/desktop/src/components/bottom_menu/BottomMenuAlertPopover.tsx index 64d9cbdfea..3513d46dbf 100644 --- a/ui/desktop/src/components/bottom_menu/BottomMenuAlertPopover.tsx +++ b/ui/desktop/src/components/bottom_menu/BottomMenuAlertPopover.tsx @@ -1,6 +1,5 @@ import React, { useRef, useEffect, useCallback } from 'react'; import { FaCircle } from 'react-icons/fa'; -import { IoIosCloseCircle } from 'react-icons/io'; import { Popover, PopoverContent, PopoverTrigger } from '../ui/popover'; import { cn } from '../../utils'; import { Alert, AlertType } from '../alerts'; @@ -91,13 +90,12 @@ export default function BottomMenuAlertPopover({ alerts }: AlertPopoverProps) { // Determine the icon and styling based on the alerts const hasError = alerts.some((alert) => alert.type === AlertType.Error); - const TriggerIcon = hasError ? IoIosCloseCircle : FaCircle; - const triggerColor = hasError ? 'text-[#d7040e]' : 'text-[#cc4b03]'; - - // Different styling for error icon vs notification dot - const iconStyles = hasError - ? 'h-5 w-5' // Keep error icon larger - : 'h-2.5 w-2.5'; // Smaller notification dot + const hasInfo = alerts.some((alert) => alert.type === AlertType.Info); + const triggerColor = hasError + ? 'text-[#d7040e]' // Red color for error alerts + : hasInfo + ? 'text-[#00b300]' // Green color for info alerts + : 'text-[#cc4b03]'; // Orange color for warning alerts return (
@@ -106,13 +104,6 @@ export default function BottomMenuAlertPopover({ alerts }: AlertPopoverProps) {
{ - if (hideTimerRef.current) { - clearTimeout(hideTimerRef.current); - } - setWasAutoShown(false); - setIsOpen(!isOpen); - }} onMouseEnter={() => { setIsOpen(true); setIsHovered(true); @@ -122,10 +113,18 @@ export default function BottomMenuAlertPopover({ alerts }: AlertPopoverProps) { } }} onMouseLeave={() => { - setIsHovered(false); + // Start a short timer to allow moving to content + hideTimerRef.current = setTimeout(() => { + if (!isHovered) { + setIsHovered(false); + setIsOpen(false); + } + }, 100); }} > - +
+ +
@@ -158,6 +157,7 @@ export default function BottomMenuAlertPopover({ alerts }: AlertPopoverProps) { }} onMouseLeave={() => { setIsHovered(false); + setIsOpen(false); }} >