Added progress info alert for displaying token usage (#2540)
This commit is contained in:
@@ -1,11 +1,12 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { IoIosCloseCircle, IoIosWarning } from 'react-icons/io';
|
import { IoIosCloseCircle, IoIosWarning, IoIosInformationCircle } from 'react-icons/io';
|
||||||
import { cn } from '../../utils';
|
import { cn } from '../../utils';
|
||||||
import { Alert, AlertType } from './types';
|
import { Alert, AlertType } from './types';
|
||||||
|
|
||||||
const alertIcons: Record<AlertType, React.ReactNode> = {
|
const alertIcons: Record<AlertType, React.ReactNode> = {
|
||||||
[AlertType.Error]: <IoIosCloseCircle className="h-5 w-5" />,
|
[AlertType.Error]: <IoIosCloseCircle className="h-5 w-5" />,
|
||||||
[AlertType.Warning]: <IoIosWarning className="h-5 w-5" />,
|
[AlertType.Warning]: <IoIosWarning className="h-5 w-5" />,
|
||||||
|
[AlertType.Info]: <IoIosInformationCircle className="h-5 w-5" />,
|
||||||
};
|
};
|
||||||
|
|
||||||
interface AlertBoxProps {
|
interface AlertBoxProps {
|
||||||
@@ -16,28 +17,73 @@ interface AlertBoxProps {
|
|||||||
const alertStyles: Record<AlertType, string> = {
|
const alertStyles: Record<AlertType, string> = {
|
||||||
[AlertType.Error]: 'bg-[#d7040e] text-white',
|
[AlertType.Error]: 'bg-[#d7040e] text-white',
|
||||||
[AlertType.Warning]: 'bg-[#cc4b03] 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) => {
|
export const AlertBox = ({ alert, className }: AlertBoxProps) => {
|
||||||
return (
|
return (
|
||||||
<div className={cn('flex items-center gap-2 px-3 py-2', alertStyles[alert.type], className)}>
|
<div className={cn('flex flex-col gap-2 px-3 py-3', alertStyles[alert.type], className)}>
|
||||||
<div className="flex-shrink-0">{alertIcons[alert.type]}</div>
|
{alert.progress ? (
|
||||||
<div className="flex flex-col gap-2 flex-1">
|
<div className="flex flex-col gap-2">
|
||||||
<span className="text-[11px] break-words whitespace-pre-line">{alert.message}</span>
|
<span className="text-[11px]">{alert.message}</span>
|
||||||
{alert.action && (
|
<div className="flex justify-between w-full">
|
||||||
<a
|
{[...Array(30)].map((_, i) => (
|
||||||
role="button"
|
<div
|
||||||
onClick={(e) => {
|
key={i}
|
||||||
e.preventDefault();
|
className={cn(
|
||||||
e.stopPropagation();
|
'h-[2px] w-[2px] rounded-full',
|
||||||
alert.action?.onClick();
|
alert.type === AlertType.Info
|
||||||
}}
|
? i < Math.round((alert.progress.current / alert.progress.total) * 30)
|
||||||
className="text-[11px] text-left underline hover:opacity-80 cursor-pointer outline-none"
|
? 'dark:bg-black bg-white'
|
||||||
>
|
: 'dark:bg-black/20 bg-white/20'
|
||||||
{alert.action.text}
|
: i < Math.round((alert.progress.current / alert.progress.total) * 30)
|
||||||
</a>
|
? 'bg-white'
|
||||||
)}
|
: 'bg-white/20'
|
||||||
</div>
|
)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between items-baseline text-[11px]">
|
||||||
|
<div className="flex gap-1 items-baseline">
|
||||||
|
<span className={'dark:text-black/60 text-white/60'}>
|
||||||
|
{alert.progress.current >= 1000
|
||||||
|
? (alert.progress.current / 1000).toFixed(1) + 'k'
|
||||||
|
: alert.progress.current}
|
||||||
|
</span>
|
||||||
|
<span className={'dark:text-black/40 text-white/40'}>
|
||||||
|
{Math.round((alert.progress.current / alert.progress.total) * 100)}%
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<span className={'dark:text-black/60 text-white/60'}>
|
||||||
|
{alert.progress.total >= 1000
|
||||||
|
? (alert.progress.total / 1000).toFixed(0) + 'k'
|
||||||
|
: alert.progress.total}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="flex-shrink-0">{alertIcons[alert.type]}</div>
|
||||||
|
<div className="flex flex-col gap-2 flex-1">
|
||||||
|
<span className="text-[11px] break-words whitespace-pre-line">{alert.message}</span>
|
||||||
|
{alert.action && (
|
||||||
|
<a
|
||||||
|
role="button"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
alert.action?.onClick();
|
||||||
|
}}
|
||||||
|
className="text-[11px] text-left underline hover:opacity-80 cursor-pointer outline-none"
|
||||||
|
>
|
||||||
|
{alert.action.text}
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
export enum AlertType {
|
export enum AlertType {
|
||||||
Error = 'error',
|
Error = 'error',
|
||||||
Warning = 'warning',
|
Warning = 'warning',
|
||||||
|
Info = 'info',
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Alert {
|
export interface Alert {
|
||||||
@@ -11,4 +12,8 @@ export interface Alert {
|
|||||||
text: string;
|
text: string;
|
||||||
onClick: () => void;
|
onClick: () => void;
|
||||||
};
|
};
|
||||||
|
progress?: {
|
||||||
|
current: number;
|
||||||
|
total: number;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,19 +1,9 @@
|
|||||||
import { useState, useCallback } from 'react';
|
import { useState, useCallback } from 'react';
|
||||||
import { Alert, AlertType } from './types';
|
import { Alert } from './types';
|
||||||
|
|
||||||
interface AlertOptions {
|
|
||||||
type: AlertType;
|
|
||||||
message: string;
|
|
||||||
action?: {
|
|
||||||
text: string;
|
|
||||||
onClick: () => void;
|
|
||||||
};
|
|
||||||
autoShow?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface UseAlerts {
|
interface UseAlerts {
|
||||||
alerts: Alert[];
|
alerts: Alert[];
|
||||||
addAlert: (options: AlertOptions) => void;
|
addAlert: (options: Alert) => void;
|
||||||
removeAlert: (index: number) => void;
|
removeAlert: (index: number) => void;
|
||||||
clearAlerts: () => void;
|
clearAlerts: () => void;
|
||||||
}
|
}
|
||||||
@@ -21,7 +11,7 @@ interface UseAlerts {
|
|||||||
export const useAlerts = (): UseAlerts => {
|
export const useAlerts = (): UseAlerts => {
|
||||||
const [alerts, setAlerts] = useState<Alert[]>([]);
|
const [alerts, setAlerts] = useState<Alert[]>([]);
|
||||||
|
|
||||||
const addAlert = useCallback((options: AlertOptions) => {
|
const addAlert = useCallback((options: Alert) => {
|
||||||
setAlerts((prev) => [...prev, options]);
|
setAlerts((prev) => [...prev, options]);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ export default function BottomMenu({
|
|||||||
const toolCount = useToolCount();
|
const toolCount = useToolCount();
|
||||||
const { getProviders, read } = useConfig();
|
const { getProviders, read } = useConfig();
|
||||||
const [tokenLimit, setTokenLimit] = useState<number>(TOKEN_LIMIT_DEFAULT);
|
const [tokenLimit, setTokenLimit] = useState<number>(TOKEN_LIMIT_DEFAULT);
|
||||||
|
const [isTokenLimitLoaded, setIsTokenLimitLoaded] = useState(false);
|
||||||
|
|
||||||
// Load model limits from the API
|
// Load model limits from the API
|
||||||
const getModelLimits = async () => {
|
const getModelLimits = async () => {
|
||||||
@@ -67,10 +68,14 @@ export default function BottomMenu({
|
|||||||
// Load providers and get current model's token limit
|
// Load providers and get current model's token limit
|
||||||
const loadProviderDetails = async () => {
|
const loadProviderDetails = async () => {
|
||||||
try {
|
try {
|
||||||
|
// Reset token limit loaded state
|
||||||
|
setIsTokenLimitLoaded(false);
|
||||||
|
|
||||||
// Get current model and provider first to avoid unnecessary provider fetches
|
// Get current model and provider first to avoid unnecessary provider fetches
|
||||||
const { model, provider } = await getCurrentModelAndProvider({ readFromConfig: read });
|
const { model, provider } = await getCurrentModelAndProvider({ readFromConfig: read });
|
||||||
if (!model || !provider) {
|
if (!model || !provider) {
|
||||||
console.log('No model or provider found');
|
console.log('No model or provider found');
|
||||||
|
setIsTokenLimitLoaded(true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -83,6 +88,7 @@ export default function BottomMenu({
|
|||||||
const modelConfig = currentProvider.metadata.known_models.find((m) => m.name === model);
|
const modelConfig = currentProvider.metadata.known_models.find((m) => m.name === model);
|
||||||
if (modelConfig?.context_limit) {
|
if (modelConfig?.context_limit) {
|
||||||
setTokenLimit(modelConfig.context_limit);
|
setTokenLimit(modelConfig.context_limit);
|
||||||
|
setIsTokenLimitLoaded(true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -92,15 +98,18 @@ export default function BottomMenu({
|
|||||||
const fallbackLimit = findModelLimit(model as string, modelLimit);
|
const fallbackLimit = findModelLimit(model as string, modelLimit);
|
||||||
if (fallbackLimit !== null) {
|
if (fallbackLimit !== null) {
|
||||||
setTokenLimit(fallbackLimit);
|
setTokenLimit(fallbackLimit);
|
||||||
|
setIsTokenLimitLoaded(true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If no match found, use the default model limit
|
// If no match found, use the default model limit
|
||||||
setTokenLimit(TOKEN_LIMIT_DEFAULT);
|
setTokenLimit(TOKEN_LIMIT_DEFAULT);
|
||||||
|
setIsTokenLimitLoaded(true);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Error loading providers or token limit:', err);
|
console.error('Error loading providers or token limit:', err);
|
||||||
// Set default limit on error
|
// Set default limit on error
|
||||||
setTokenLimit(TOKEN_LIMIT_DEFAULT);
|
setTokenLimit(TOKEN_LIMIT_DEFAULT);
|
||||||
|
setIsTokenLimitLoaded(true);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -110,24 +119,36 @@ export default function BottomMenu({
|
|||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [currentModel]);
|
}, [currentModel]);
|
||||||
|
|
||||||
// Handle tool count alerts
|
// Handle tool count alerts and token usage
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
clearAlerts();
|
clearAlerts();
|
||||||
|
|
||||||
// Add token alerts if we have a token limit
|
// Only show token alerts if we have loaded the real token limit
|
||||||
if (tokenLimit && numTokens > 0) {
|
if (isTokenLimitLoaded && tokenLimit && numTokens > 0) {
|
||||||
if (numTokens >= tokenLimit) {
|
if (numTokens >= tokenLimit) {
|
||||||
|
// Only show error alert when limit reached
|
||||||
addAlert({
|
addAlert({
|
||||||
type: AlertType.Error,
|
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
|
autoShow: true, // Auto-show token limit errors
|
||||||
});
|
});
|
||||||
} else if (numTokens >= tokenLimit * TOKEN_WARNING_THRESHOLD) {
|
} else if (numTokens >= tokenLimit * TOKEN_WARNING_THRESHOLD) {
|
||||||
|
// Only show warning alert when approaching limit
|
||||||
addAlert({
|
addAlert({
|
||||||
type: AlertType.Warning,
|
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
|
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
|
// We intentionally omit setView as it shouldn't trigger a re-render of alerts
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// 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
|
// Add effect to handle clicks outside
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import React, { useRef, useEffect, useCallback } from 'react';
|
import React, { useRef, useEffect, useCallback } from 'react';
|
||||||
import { FaCircle } from 'react-icons/fa';
|
import { FaCircle } from 'react-icons/fa';
|
||||||
import { IoIosCloseCircle } from 'react-icons/io';
|
|
||||||
import { Popover, PopoverContent, PopoverTrigger } from '../ui/popover';
|
import { Popover, PopoverContent, PopoverTrigger } from '../ui/popover';
|
||||||
import { cn } from '../../utils';
|
import { cn } from '../../utils';
|
||||||
import { Alert, AlertType } from '../alerts';
|
import { Alert, AlertType } from '../alerts';
|
||||||
@@ -91,13 +90,12 @@ export default function BottomMenuAlertPopover({ alerts }: AlertPopoverProps) {
|
|||||||
|
|
||||||
// Determine the icon and styling based on the alerts
|
// Determine the icon and styling based on the alerts
|
||||||
const hasError = alerts.some((alert) => alert.type === AlertType.Error);
|
const hasError = alerts.some((alert) => alert.type === AlertType.Error);
|
||||||
const TriggerIcon = hasError ? IoIosCloseCircle : FaCircle;
|
const hasInfo = alerts.some((alert) => alert.type === AlertType.Info);
|
||||||
const triggerColor = hasError ? 'text-[#d7040e]' : 'text-[#cc4b03]';
|
const triggerColor = hasError
|
||||||
|
? 'text-[#d7040e]' // Red color for error alerts
|
||||||
// Different styling for error icon vs notification dot
|
: hasInfo
|
||||||
const iconStyles = hasError
|
? 'text-[#00b300]' // Green color for info alerts
|
||||||
? 'h-5 w-5' // Keep error icon larger
|
: 'text-[#cc4b03]'; // Orange color for warning alerts
|
||||||
: 'h-2.5 w-2.5'; // Smaller notification dot
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={popoverRef}>
|
<div ref={popoverRef}>
|
||||||
@@ -106,13 +104,6 @@ export default function BottomMenuAlertPopover({ alerts }: AlertPopoverProps) {
|
|||||||
<PopoverTrigger asChild>
|
<PopoverTrigger asChild>
|
||||||
<div
|
<div
|
||||||
className="cursor-pointer flex items-center justify-center min-w-5 min-h-5 translate-y-[1px]"
|
className="cursor-pointer flex items-center justify-center min-w-5 min-h-5 translate-y-[1px]"
|
||||||
onClick={() => {
|
|
||||||
if (hideTimerRef.current) {
|
|
||||||
clearTimeout(hideTimerRef.current);
|
|
||||||
}
|
|
||||||
setWasAutoShown(false);
|
|
||||||
setIsOpen(!isOpen);
|
|
||||||
}}
|
|
||||||
onMouseEnter={() => {
|
onMouseEnter={() => {
|
||||||
setIsOpen(true);
|
setIsOpen(true);
|
||||||
setIsHovered(true);
|
setIsHovered(true);
|
||||||
@@ -122,10 +113,18 @@ export default function BottomMenuAlertPopover({ alerts }: AlertPopoverProps) {
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
onMouseLeave={() => {
|
onMouseLeave={() => {
|
||||||
setIsHovered(false);
|
// Start a short timer to allow moving to content
|
||||||
|
hideTimerRef.current = setTimeout(() => {
|
||||||
|
if (!isHovered) {
|
||||||
|
setIsHovered(false);
|
||||||
|
setIsOpen(false);
|
||||||
|
}
|
||||||
|
}, 100);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<TriggerIcon className={cn(iconStyles, triggerColor)} />
|
<div className={cn('relative', '-right-1', triggerColor)}>
|
||||||
|
<FaCircle size={5} />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
|
|
||||||
@@ -158,6 +157,7 @@ export default function BottomMenuAlertPopover({ alerts }: AlertPopoverProps) {
|
|||||||
}}
|
}}
|
||||||
onMouseLeave={() => {
|
onMouseLeave={() => {
|
||||||
setIsHovered(false);
|
setIsHovered(false);
|
||||||
|
setIsOpen(false);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
|
|||||||
Reference in New Issue
Block a user