diff --git a/client/src/components/Chat/ChatView.tsx b/client/src/components/Chat/ChatView.tsx index 9c760e4400..f19e33904d 100644 --- a/client/src/components/Chat/ChatView.tsx +++ b/client/src/components/Chat/ChatView.tsx @@ -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({ defaultValues: { text: '' }, }); @@ -69,7 +106,70 @@ function ChatView({ index = 0 }: { index?: number }) { } else if ((isLoading || isNavigating) && !isLandingPage) { content = ; } else if (!isLandingPage) { - content = ; + content = ( + +
+
+
+ + + + {conversationCosts.totals.prompt.tokenCount}t +
+
${Math.abs(conversationCosts.totals.prompt.usd).toFixed(6)}
+
+
+
{conversationCosts.totals.total.tokenCount}t
+
${Math.abs(conversationCosts.totals.total.usd).toFixed(6)}
+
+
+
+ + + + {conversationCosts.totals.completion.tokenCount}t +
+
${Math.abs(conversationCosts.totals.completion.usd).toFixed(6)}
+
+
+ + ) + } + /> + ); } else { content = ; } diff --git a/client/src/components/Chat/Messages/MessagesView.tsx b/client/src/components/Chat/Messages/MessagesView.tsx index bc6e285a57..6a27d0fd2d 100644 --- a/client/src/components/Chat/Messages/MessagesView.tsx +++ b/client/src/components/Chat/Messages/MessagesView.tsx @@ -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({ + {costBar && ( +
+ {costBar} +
+ )} + { + 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', )} > -

{messageLabel}

+

+ {messageLabel} + {perMessageCost && ( + + {perMessageCost.tokenCount > 0 && ( + + {perMessageCost.tokenType === 'prompt' ? ( + + + + ) : ( + + + + )} + {perMessageCost.tokenCount}t + + )} + ${Math.abs(perMessageCost.usd).toFixed(6)} + + )} +

diff --git a/client/src/components/Messages/ContentRender.tsx b/client/src/components/Messages/ContentRender.tsx index daeccbc130..e383b47cb1 100644 --- a/client/src/components/Messages/ContentRender.tsx +++ b/client/src/components/Messages/ContentRender.tsx @@ -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', )} > -

{messageLabel}

+

+ {messageLabel} + {perMessageCost && ( + + {perMessageCost.tokenCount > 0 && ( + + {perMessageCost.tokenType === 'prompt' ? ( + + + + ) : ( + + + + )} + {perMessageCost.tokenCount}t + + )} + ${Math.abs(perMessageCost.usd).toFixed(6)} + + )} +