mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-04-07 00:15:23 +02:00
199 lines
6 KiB
TypeScript
199 lines
6 KiB
TypeScript
|
|
import { useEffect, useMemo, useRef, useState } from 'react';
|
||
|
|
import { useQueryClient } from '@tanstack/react-query';
|
||
|
|
import { QueryKeys, Constants } from 'librechat-data-provider';
|
||
|
|
import type { TConversation, TMessage, TModelSpec, TStartupConfig } from 'librechat-data-provider';
|
||
|
|
import { useRecoilValue } from 'recoil';
|
||
|
|
import { TooltipAnchor } from '@librechat/client';
|
||
|
|
import { useGetStartupConfig } from '~/data-provider';
|
||
|
|
import { useLocalize } from '~/hooks';
|
||
|
|
import store from '~/store';
|
||
|
|
import { cn } from '~/utils';
|
||
|
|
|
||
|
|
type ContextTrackerProps = {
|
||
|
|
conversation: TConversation | null;
|
||
|
|
getMessages: () => TMessage[] | undefined;
|
||
|
|
isSubmitting: boolean;
|
||
|
|
};
|
||
|
|
|
||
|
|
type MessageWithTokenCount = TMessage & { tokenCount?: number };
|
||
|
|
|
||
|
|
const TRACKER_SIZE = 24;
|
||
|
|
const TRACKER_STROKE = 2.5;
|
||
|
|
|
||
|
|
const formatTokenCount = (count: number): string => {
|
||
|
|
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 getUsedTokens = (messages: TMessage[] | undefined): number => {
|
||
|
|
if (!messages?.length) {
|
||
|
|
return 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
return messages.reduce((totalTokens, message) => {
|
||
|
|
const tokenCount = (message as MessageWithTokenCount).tokenCount;
|
||
|
|
if (typeof tokenCount !== 'number' || !Number.isFinite(tokenCount) || tokenCount <= 0) {
|
||
|
|
return totalTokens;
|
||
|
|
}
|
||
|
|
|
||
|
|
return totalTokens + tokenCount;
|
||
|
|
}, 0);
|
||
|
|
};
|
||
|
|
|
||
|
|
const getSpecMaxContextTokens = (
|
||
|
|
startupConfig: TStartupConfig | undefined,
|
||
|
|
specName: string | null | undefined,
|
||
|
|
): number | null => {
|
||
|
|
if (!specName) {
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
|
||
|
|
const modelSpec = startupConfig?.modelSpecs?.list?.find(
|
||
|
|
(spec: TModelSpec) => spec.name === specName,
|
||
|
|
);
|
||
|
|
const maxContextTokens = modelSpec?.preset?.maxContextTokens;
|
||
|
|
if (
|
||
|
|
typeof maxContextTokens !== 'number' ||
|
||
|
|
!Number.isFinite(maxContextTokens) ||
|
||
|
|
maxContextTokens <= 0
|
||
|
|
) {
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
|
||
|
|
return maxContextTokens;
|
||
|
|
};
|
||
|
|
|
||
|
|
export default function ContextTracker({
|
||
|
|
conversation,
|
||
|
|
getMessages,
|
||
|
|
isSubmitting,
|
||
|
|
}: ContextTrackerProps) {
|
||
|
|
const localize = useLocalize();
|
||
|
|
const queryClient = useQueryClient();
|
||
|
|
const { data: startupConfig } = useGetStartupConfig();
|
||
|
|
const showContextTracker = useRecoilValue(store.showContextTracker);
|
||
|
|
const conversationId = conversation?.conversationId ?? Constants.NEW_CONVO;
|
||
|
|
const [usedTokens, setUsedTokens] = useState<number>(() => getUsedTokens(getMessages()));
|
||
|
|
|
||
|
|
useEffect(() => {
|
||
|
|
setUsedTokens(getUsedTokens(getMessages()));
|
||
|
|
}, [conversationId, getMessages]);
|
||
|
|
|
||
|
|
useEffect(() => {
|
||
|
|
const unsubscribe = queryClient.getQueryCache().subscribe((event) => {
|
||
|
|
const queryKey = event?.query?.queryKey;
|
||
|
|
if (!Array.isArray(queryKey) || queryKey[0] !== QueryKeys.messages) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
setUsedTokens(getUsedTokens(getMessages()));
|
||
|
|
});
|
||
|
|
|
||
|
|
return unsubscribe;
|
||
|
|
}, [getMessages, queryClient]);
|
||
|
|
|
||
|
|
const prevIsSubmitting = useRef(isSubmitting);
|
||
|
|
useEffect(() => {
|
||
|
|
if (prevIsSubmitting.current && !isSubmitting) {
|
||
|
|
setUsedTokens(getUsedTokens(getMessages()));
|
||
|
|
}
|
||
|
|
prevIsSubmitting.current = isSubmitting;
|
||
|
|
}, [isSubmitting, getMessages]);
|
||
|
|
|
||
|
|
const maxContextTokens =
|
||
|
|
typeof conversation?.maxContextTokens === 'number' &&
|
||
|
|
Number.isFinite(conversation.maxContextTokens) &&
|
||
|
|
conversation.maxContextTokens > 0
|
||
|
|
? conversation.maxContextTokens
|
||
|
|
: getSpecMaxContextTokens(startupConfig, conversation?.spec);
|
||
|
|
|
||
|
|
const usageRatio = useMemo(() => {
|
||
|
|
if (maxContextTokens == null || maxContextTokens <= 0) {
|
||
|
|
return 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
return Math.min(usedTokens / maxContextTokens, 1);
|
||
|
|
}, [maxContextTokens, usedTokens]);
|
||
|
|
|
||
|
|
const trackerRadius = useMemo(() => (TRACKER_SIZE - TRACKER_STROKE) / 2, []);
|
||
|
|
const circumference = useMemo(() => 2 * Math.PI * trackerRadius, [trackerRadius]);
|
||
|
|
const dashOffset = useMemo(() => circumference * (1 - usageRatio), [circumference, usageRatio]);
|
||
|
|
|
||
|
|
let ringColorClass = 'text-text-primary';
|
||
|
|
if (maxContextTokens == null) {
|
||
|
|
ringColorClass = 'text-text-secondary';
|
||
|
|
} else if (usageRatio > 0.9) {
|
||
|
|
ringColorClass = 'text-red-500';
|
||
|
|
} else if (usageRatio > 0.75) {
|
||
|
|
ringColorClass = 'text-yellow-500';
|
||
|
|
}
|
||
|
|
|
||
|
|
const tooltipDescription = useMemo(() => {
|
||
|
|
if (maxContextTokens == null) {
|
||
|
|
return localize('com_ui_context_usage_unknown_max', {
|
||
|
|
0: formatTokenCount(usedTokens),
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
const percentage = (usageRatio * 100).toFixed(1) + '%';
|
||
|
|
return localize('com_ui_context_usage_with_max', {
|
||
|
|
0: percentage,
|
||
|
|
1: formatTokenCount(usedTokens),
|
||
|
|
2: formatTokenCount(maxContextTokens),
|
||
|
|
});
|
||
|
|
}, [localize, maxContextTokens, usedTokens, usageRatio]);
|
||
|
|
|
||
|
|
if (!showContextTracker) {
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
|
||
|
|
return (
|
||
|
|
<TooltipAnchor
|
||
|
|
description={tooltipDescription}
|
||
|
|
side="top"
|
||
|
|
render={
|
||
|
|
<button
|
||
|
|
type="button"
|
||
|
|
aria-label={localize('com_ui_context_usage')}
|
||
|
|
className={cn('rounded-full p-1.5', ringColorClass)}
|
||
|
|
data-testid="context-tracker"
|
||
|
|
>
|
||
|
|
<svg
|
||
|
|
width={TRACKER_SIZE}
|
||
|
|
height={TRACKER_SIZE}
|
||
|
|
viewBox={`0 0 ${TRACKER_SIZE} ${TRACKER_SIZE}`}
|
||
|
|
>
|
||
|
|
<circle
|
||
|
|
cx={TRACKER_SIZE / 2}
|
||
|
|
cy={TRACKER_SIZE / 2}
|
||
|
|
r={trackerRadius}
|
||
|
|
stroke="currentColor"
|
||
|
|
strokeWidth={TRACKER_STROKE}
|
||
|
|
fill="transparent"
|
||
|
|
className="opacity-20"
|
||
|
|
/>
|
||
|
|
<circle
|
||
|
|
cx={TRACKER_SIZE / 2}
|
||
|
|
cy={TRACKER_SIZE / 2}
|
||
|
|
r={trackerRadius}
|
||
|
|
stroke="currentColor"
|
||
|
|
strokeWidth={TRACKER_STROKE}
|
||
|
|
strokeLinecap="round"
|
||
|
|
strokeDasharray={`${circumference} ${circumference}`}
|
||
|
|
strokeDashoffset={dashOffset}
|
||
|
|
fill="transparent"
|
||
|
|
transform={`rotate(-90 ${TRACKER_SIZE / 2} ${TRACKER_SIZE / 2})`}
|
||
|
|
className="transition-all duration-200"
|
||
|
|
/>
|
||
|
|
</svg>
|
||
|
|
</button>
|
||
|
|
}
|
||
|
|
/>
|
||
|
|
);
|
||
|
|
}
|