mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-01-01 16:18:51 +01:00
feat: add frontend component for displaying convo total, per message cost and tokens, and hide total block on scroll
This commit is contained in:
parent
c1b0f13360
commit
3b1c07ff46
4 changed files with 225 additions and 5 deletions
|
|
@ -1,4 +1,4 @@
|
|||
import { memo, useCallback } from 'react';
|
||||
import { memo, useCallback, useState, useEffect, useRef } from 'react';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { Spinner } from '@librechat/client';
|
||||
|
|
@ -7,9 +7,9 @@ import { Constants, buildTree } from 'librechat-data-provider';
|
|||
import type { TMessage } from 'librechat-data-provider';
|
||||
import type { ChatFormValues } from '~/common';
|
||||
import { ChatContext, AddedChatContext, useFileMapContext, ChatFormProvider } from '~/Providers';
|
||||
import { useGetMessagesByConvoId, useGetConversationCosts } from '~/data-provider';
|
||||
import { useChatHelpers, useAddedResponse, useSSE } from '~/hooks';
|
||||
import ConversationStarters from './Input/ConversationStarters';
|
||||
import { useGetMessagesByConvoId } from '~/data-provider';
|
||||
import MessagesView from './Messages/MessagesView';
|
||||
import Presentation from './Presentation';
|
||||
import ChatForm from './Input/ChatForm';
|
||||
|
|
@ -37,6 +37,9 @@ function ChatView({ index = 0 }: { index?: number }) {
|
|||
|
||||
const fileMap = useFileMapContext();
|
||||
|
||||
const [showCostBar, setShowCostBar] = useState(true);
|
||||
const lastScrollY = useRef(0);
|
||||
|
||||
const { data: messagesTree = null, isLoading } = useGetMessagesByConvoId(conversationId ?? '', {
|
||||
select: useCallback(
|
||||
(data: TMessage[]) => {
|
||||
|
|
@ -48,12 +51,46 @@ function ChatView({ index = 0 }: { index?: number }) {
|
|||
enabled: !!fileMap,
|
||||
});
|
||||
|
||||
const { data: conversationCosts } = useGetConversationCosts(conversationId ?? '', {
|
||||
enabled: !!conversationId && conversationId !== Constants.NEW_CONVO,
|
||||
});
|
||||
|
||||
const chatHelpers = useChatHelpers(index, conversationId);
|
||||
const addedChatHelpers = useAddedResponse({ rootIndex: index });
|
||||
|
||||
useSSE(rootSubmission, chatHelpers, false);
|
||||
useSSE(addedSubmission, addedChatHelpers, true);
|
||||
|
||||
useEffect(() => {
|
||||
const handleScroll = (event: Event) => {
|
||||
const target = event.target as HTMLElement;
|
||||
const currentScrollY = target.scrollTop;
|
||||
const scrollHeight = target.scrollHeight;
|
||||
const clientHeight = target.clientHeight;
|
||||
|
||||
const distanceFromBottom = scrollHeight - currentScrollY - clientHeight;
|
||||
const isAtBottom = distanceFromBottom < 10;
|
||||
|
||||
setShowCostBar(isAtBottom);
|
||||
lastScrollY.current = currentScrollY;
|
||||
};
|
||||
|
||||
const findAndAttachScrollListener = () => {
|
||||
const messagesContainer = document.querySelector('[class*="scrollbar-gutter-stable"]');
|
||||
if (messagesContainer) {
|
||||
messagesContainer.addEventListener('scroll', handleScroll, { passive: true });
|
||||
return () => {
|
||||
messagesContainer.removeEventListener('scroll', handleScroll);
|
||||
};
|
||||
}
|
||||
setTimeout(findAndAttachScrollListener, 100);
|
||||
};
|
||||
|
||||
const cleanup = findAndAttachScrollListener();
|
||||
|
||||
return cleanup;
|
||||
}, [messagesTree]);
|
||||
|
||||
const methods = useForm<ChatFormValues>({
|
||||
defaultValues: { text: '' },
|
||||
});
|
||||
|
|
@ -69,7 +106,70 @@ function ChatView({ index = 0 }: { index?: number }) {
|
|||
} else if ((isLoading || isNavigating) && !isLandingPage) {
|
||||
content = <LoadingSpinner />;
|
||||
} else if (!isLandingPage) {
|
||||
content = <MessagesView messagesTree={messagesTree} />;
|
||||
content = (
|
||||
<MessagesView
|
||||
messagesTree={messagesTree}
|
||||
costBar={
|
||||
!isLandingPage &&
|
||||
conversationCosts &&
|
||||
conversationCosts.totals && (
|
||||
<div
|
||||
className={cn(
|
||||
'mx-auto w-full max-w-md px-4 text-xs text-muted-foreground transition-all duration-300 ease-in-out',
|
||||
showCostBar ? 'opacity-100' : 'opacity-0',
|
||||
)}
|
||||
>
|
||||
<div className="grid grid-cols-3 gap-2 text-center">
|
||||
<div>
|
||||
<div>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="1em"
|
||||
height="1em"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
className="inline"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M11.293 5.293a1 1 0 0 1 1.414 0l5 5a1 1 0 0 1-1.414 1.414L13 8.414V18a1 1 0 1 1-2 0V8.414l-3.293 3.293a1 1 0 0 1-1.414-1.414l5-5Z"
|
||||
clipRule="evenodd"
|
||||
></path>
|
||||
</svg>
|
||||
{conversationCosts.totals.prompt.tokenCount}t
|
||||
</div>
|
||||
<div>${Math.abs(conversationCosts.totals.prompt.usd).toFixed(6)}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div>{conversationCosts.totals.total.tokenCount}t</div>
|
||||
<div>${Math.abs(conversationCosts.totals.total.usd).toFixed(6)}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="1em"
|
||||
height="1em"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
className="inline"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M12.707 18.707a1 1 0 0 1-1.414 0l-5-5a1 1 0 1 1 1.414-1.414L11 15.586V6a1 1 0 1 1 2 0v9.586l3.293-3.293a1 1 0 0 1 1.414 1.414l-5 5Z"
|
||||
clipRule="evenodd"
|
||||
></path>
|
||||
</svg>
|
||||
{conversationCosts.totals.completion.tokenCount}t
|
||||
</div>
|
||||
<div>${Math.abs(conversationCosts.totals.completion.usd).toFixed(6)}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
content = <Landing centerFormOnLanding={centerFormOnLanding} />;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,8 +10,10 @@ import store from '~/store';
|
|||
|
||||
export default function MessagesView({
|
||||
messagesTree: _messagesTree,
|
||||
costBar,
|
||||
}: {
|
||||
messagesTree?: TMessage[] | null;
|
||||
costBar?: React.ReactNode;
|
||||
}) {
|
||||
const localize = useLocalize();
|
||||
const fontSize = useRecoilValue(store.fontSize);
|
||||
|
|
@ -75,6 +77,12 @@ export default function MessagesView({
|
|||
</div>
|
||||
</div>
|
||||
|
||||
{costBar && (
|
||||
<div className="pointer-events-none absolute bottom-2 left-1/2 z-10 -translate-x-1/2">
|
||||
{costBar}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<CSSTransition
|
||||
in={showScrollButton && scrollButtonPreference}
|
||||
timeout={{
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import PlaceholderRow from '~/components/Chat/Messages/ui/PlaceholderRow';
|
|||
import SiblingSwitch from '~/components/Chat/Messages/SiblingSwitch';
|
||||
import HoverButtons from '~/components/Chat/Messages/HoverButtons';
|
||||
import MessageIcon from '~/components/Chat/Messages/MessageIcon';
|
||||
import { useGetConversationCosts } from '~/data-provider';
|
||||
import { Plugin } from '~/components/Messages/Content';
|
||||
import SubRow from '~/components/Chat/Messages/SubRow';
|
||||
import { MessageContext } from '~/Providers';
|
||||
|
|
@ -60,6 +61,21 @@ const MessageRender = memo(
|
|||
});
|
||||
const maximizeChatSpace = useRecoilValue(store.maximizeChatSpace);
|
||||
const fontSize = useRecoilValue(store.fontSize);
|
||||
const convoId = conversation?.conversationId ?? '';
|
||||
const { data: convoCosts } = useGetConversationCosts(convoId, {
|
||||
enabled: !!convoId,
|
||||
});
|
||||
|
||||
const perMessageCost = useMemo(() => {
|
||||
if (!convoCosts || !convoCosts.perMessage || !msg?.messageId) {
|
||||
return null;
|
||||
}
|
||||
const entry = convoCosts.perMessage.find((p) => p.messageId === msg.messageId);
|
||||
if (!entry) {
|
||||
return null;
|
||||
}
|
||||
return entry;
|
||||
}, [convoCosts, msg?.messageId]);
|
||||
|
||||
const handleRegenerateMessage = useCallback(() => regenerateMessage(), [regenerateMessage]);
|
||||
const hasNoChildren = !(msg?.children?.length ?? 0);
|
||||
|
|
@ -157,7 +173,50 @@ const MessageRender = memo(
|
|||
msg.isCreatedByUser ? 'user-turn' : 'agent-turn',
|
||||
)}
|
||||
>
|
||||
<h2 className={cn('select-none font-semibold', fontSize)}>{messageLabel}</h2>
|
||||
<h2 className={cn('select-none font-semibold', fontSize)}>
|
||||
{messageLabel}
|
||||
{perMessageCost && (
|
||||
<span className="ml-2 inline-flex items-center gap-2 px-2 py-0.5 text-xs text-muted-foreground">
|
||||
{perMessageCost.tokenCount > 0 && (
|
||||
<span>
|
||||
{perMessageCost.tokenType === 'prompt' ? (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="1em"
|
||||
height="1em"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
className="inline"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M11.293 5.293a1 1 0 0 1 1.414 0l5 5a1 1 0 0 1-1.414 1.414L13 8.414V18a1 1 0 1 1-2 0V8.414l-3.293 3.293a1 1 0 0 1-1.414-1.414l5-5Z"
|
||||
clipRule="evenodd"
|
||||
></path>
|
||||
</svg>
|
||||
) : (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="1em"
|
||||
height="1em"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
className="inline"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M12.707 18.707a1 1 0 0 1-1.414 0l-5-5a1 1 0 1 1 1.414-1.414L11 15.586V6a1 1 0 1 1 2 0v9.586l3.293-3.293a1 1 0 0 1 1.414 1.414l-5 5Z"
|
||||
clipRule="evenodd"
|
||||
></path>
|
||||
</svg>
|
||||
)}
|
||||
{perMessageCost.tokenCount}t
|
||||
</span>
|
||||
)}
|
||||
<span className="whitespace-pre">${Math.abs(perMessageCost.usd).toFixed(6)}</span>
|
||||
</span>
|
||||
)}
|
||||
</h2>
|
||||
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="flex max-w-full flex-grow flex-col gap-0">
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import SiblingSwitch from '~/components/Chat/Messages/SiblingSwitch';
|
|||
import HoverButtons from '~/components/Chat/Messages/HoverButtons';
|
||||
import MessageIcon from '~/components/Chat/Messages/MessageIcon';
|
||||
import { useAttachments, useMessageActions } from '~/hooks';
|
||||
import { useGetConversationCosts } from '~/data-provider';
|
||||
import SubRow from '~/components/Chat/Messages/SubRow';
|
||||
import { cn, logger } from '~/utils';
|
||||
import store from '~/store';
|
||||
|
|
@ -62,6 +63,15 @@ const ContentRender = memo(
|
|||
});
|
||||
const maximizeChatSpace = useRecoilValue(store.maximizeChatSpace);
|
||||
const fontSize = useRecoilValue(store.fontSize);
|
||||
const convoId = conversation?.conversationId ?? '';
|
||||
const { data: convoCosts } = useGetConversationCosts(convoId, { enabled: !!convoId });
|
||||
|
||||
const perMessageCost = useMemo(() => {
|
||||
if (!convoCosts || !convoCosts.perMessage || !msg?.messageId) {
|
||||
return null;
|
||||
}
|
||||
return convoCosts.perMessage.find((p) => p.messageId === msg.messageId) ?? null;
|
||||
}, [convoCosts, msg?.messageId]);
|
||||
|
||||
const handleRegenerateMessage = useCallback(() => regenerateMessage(), [regenerateMessage]);
|
||||
const isLast = useMemo(
|
||||
|
|
@ -159,7 +169,50 @@ const ContentRender = memo(
|
|||
msg.isCreatedByUser ? 'user-turn' : 'agent-turn',
|
||||
)}
|
||||
>
|
||||
<h2 className={cn('select-none font-semibold', fontSize)}>{messageLabel}</h2>
|
||||
<h2 className={cn('select-none font-semibold', fontSize)}>
|
||||
{messageLabel}
|
||||
{perMessageCost && (
|
||||
<span className="ml-2 inline-flex items-center gap-2 px-2 py-0.5 text-xs text-muted-foreground">
|
||||
{perMessageCost.tokenCount > 0 && (
|
||||
<span className="mr-2">
|
||||
{perMessageCost.tokenType === 'prompt' ? (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="1em"
|
||||
height="1em"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
className="inline"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M11.293 5.293a1 1 0 0 1 1.414 0l5 5a1 1 0 0 1-1.414 1.414L13 8.414V18a1 1 0 1 1-2 0V8.414l-3.293 3.293a1 1 0 0 1-1.414-1.414l5-5Z"
|
||||
clipRule="evenodd"
|
||||
></path>
|
||||
</svg>
|
||||
) : (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="1em"
|
||||
height="1em"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
className="inline"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M12.707 18.707a1 1 0 0 1-1.414 0l-5-5a1 1 0 1 1 1.414-1.414L11 15.586V6a1 1 0 1 1 2 0v9.586l3.293-3.293a1 1 0 0 1 1.414 1.414l-5 5Z"
|
||||
clipRule="evenodd"
|
||||
></path>
|
||||
</svg>
|
||||
)}
|
||||
{perMessageCost.tokenCount}t
|
||||
</span>
|
||||
)}
|
||||
<span className="whitespace-pre">${Math.abs(perMessageCost.usd).toFixed(6)}</span>
|
||||
</span>
|
||||
)}
|
||||
</h2>
|
||||
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="flex max-w-full flex-grow flex-col gap-0">
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue