diff --git a/web/src/api/buster_rest/chats/queryRequests.ts b/web/src/api/buster_rest/chats/queryRequests.ts index 2fe993139..bffd436cc 100644 --- a/web/src/api/buster_rest/chats/queryRequests.ts +++ b/web/src/api/buster_rest/chats/queryRequests.ts @@ -61,11 +61,23 @@ export const useGetListLogs = ( }); }; -export const useGetChat = (params: Parameters[0]) => { - const queryFn = useMemoizedFn(async () => { - return await getChat(params).then((chat) => { - console.log('TODO move this to put message in a better spot'); - return updateChatToIChat(chat, true).iChat; +export const useGetChat = ( + params: Parameters[0], + select?: (chat: IBusterChat) => TData +) => { + const queryClient = useQueryClient(); + const queryFn = useMemoizedFn(() => { + return getChat(params).then((chat) => { + const { iChat, iChatMessages } = updateChatToIChat(chat, false); + + iChat.message_ids.forEach((messageId) => { + queryClient.setQueryData( + queryKeys.chatsMessages(messageId).queryKey, + iChatMessages[messageId] + ); + }); + + return iChat; }); }); @@ -75,10 +87,11 @@ export const useGetChat = (params: Parameters[0]) => { enabled: !!params.id }); - return useQuery({ + return useQuery({ ...queryKeys.chatsGetChat(params.id), - queryKey: queryKeys.chatsGetChat(params.id).queryKey, - enabled: !!params.id + enabled: !!params.id, + queryFn, + select }); }; diff --git a/web/src/api/buster_rest/users/queryRequests.ts b/web/src/api/buster_rest/users/queryRequests.ts index 575ee1e64..f5a058624 100644 --- a/web/src/api/buster_rest/users/queryRequests.ts +++ b/web/src/api/buster_rest/users/queryRequests.ts @@ -16,20 +16,12 @@ import { import { useMemoizedFn } from '@/hooks'; import { QueryClient, useQueryClient } from '@tanstack/react-query'; import { queryKeys } from '@/api/query_keys'; -import type { - UsersFavoritePostPayload, - UserFavoriteDeletePayload, - UserUpdateFavoritesPayload, - UserRequestUserListPayload -} from '@/api/request_interfaces/user/interfaces'; +import type { UserRequestUserListPayload } from '@/api/request_interfaces/user/interfaces'; export const useGetMyUserInfo = () => { - const queryFn = useMemoizedFn(async () => { - return getMyUserInfo(); - }); return useQuery({ ...queryKeys.userGetUserMyself, - queryFn, + queryFn: getMyUserInfo, enabled: false //This is a server only query }); }; diff --git a/web/src/api/query_keys/users.ts b/web/src/api/query_keys/users.ts index fd53abd14..1ae9c56a8 100644 --- a/web/src/api/query_keys/users.ts +++ b/web/src/api/query_keys/users.ts @@ -18,7 +18,8 @@ const favoritesGetList = queryOptions({ }); const userGetUserMyself = queryOptions({ - queryKey: ['users', 'myself'] as const + queryKey: ['users', 'myself'] as const, + staleTime: 1000 * 60 * 60 // 1 hour }); const userGetUser = (userId: string) => diff --git a/web/src/components/ui/dropdown/DropdownBase.tsx b/web/src/components/ui/dropdown/DropdownBase.tsx index 0c000ae09..b684df16e 100644 --- a/web/src/components/ui/dropdown/DropdownBase.tsx +++ b/web/src/components/ui/dropdown/DropdownBase.tsx @@ -100,7 +100,7 @@ const DropdownMenuItem = React.forwardRef< = { title: 'UI/Layouts/AppPageLayout', @@ -53,6 +54,7 @@ export const LongContent: Story = { args: { header:
Header Content
, scrollable: true, + headerBorderVariant: 'ghost', children: ( <> {Array.from({ length: 100 }, (_, i) => ( diff --git a/web/src/components/ui/layouts/AppPageLayout.tsx b/web/src/components/ui/layouts/AppPageLayout.tsx index 3e89c3354..d3abaff71 100644 --- a/web/src/components/ui/layouts/AppPageLayout.tsx +++ b/web/src/components/ui/layouts/AppPageLayout.tsx @@ -39,7 +39,14 @@ export const AppPageLayout: React.FC< {header} )} - {children} + + + {header && scrollable && headerBorderVariant === 'ghost' && ( +
+ )} + + {children} +
); }; diff --git a/web/src/components/ui/layouts/AppPageLayoutHeader.tsx b/web/src/components/ui/layouts/AppPageLayoutHeader.tsx index 5b58eaa18..eb8a88e3d 100644 --- a/web/src/components/ui/layouts/AppPageLayoutHeader.tsx +++ b/web/src/components/ui/layouts/AppPageLayoutHeader.tsx @@ -3,7 +3,7 @@ import React from 'react'; import { cva, type VariantProps } from 'class-variance-authority'; const headerVariants = cva( - 'bg-page-background flex max-h-[38px] min-h-[38px] items-center justify-between gap-x-2.5 ', + 'bg-page-background flex max-h-[38px] min-h-[38px] items-center justify-between gap-x-2.5 relative z-10', { variants: { sizeVariant: { diff --git a/web/src/components/ui/typography/AppMarkdown/AppMarkdown.tsx b/web/src/components/ui/typography/AppMarkdown/AppMarkdown.tsx index 7ab3415fc..c21ffa9f9 100644 --- a/web/src/components/ui/typography/AppMarkdown/AppMarkdown.tsx +++ b/web/src/components/ui/typography/AppMarkdown/AppMarkdown.tsx @@ -70,7 +70,7 @@ const AppMarkdownBase: React.FC<{ }, []); return ( -
+
= ({ children, markdown, showLoader, ...rest }) => { if (Array.isArray(children)) { return ( -

+

{children}

); @@ -48,7 +52,13 @@ export const CustomParagraph: React.FC< } return ( -

{children}

+

+ {children} +

); }; @@ -112,7 +122,11 @@ export const CustomListItem: React.FC< } & ExtraPropsExtra > = ({ children, markdown, showLoader, ...rest }) => { return ( -
  • +
  • {children}
  • ); diff --git a/web/src/context/Chats/ChatProvider/useChatUpdate.ts b/web/src/context/Chats/ChatProvider/useChatUpdate.ts index 1ec0d983a..45b4b1543 100644 --- a/web/src/context/Chats/ChatProvider/useChatUpdate.ts +++ b/web/src/context/Chats/ChatProvider/useChatUpdate.ts @@ -38,12 +38,12 @@ export const useChatUpdate = () => { const options = queryKeys.chatsMessages(newMessageConfig.id); const queryKey = options.queryKey; const currentData = queryClient.getQueryData(queryKey); - - const iChatMessage = create(currentData!, (draft) => { - Object.assign(draft, newMessageConfig); - }); - - queryClient.setQueryData(queryKey, iChatMessage); + if (currentData) { + const iChatMessage = create(currentData, (draft) => { + Object.assign(draft, newMessageConfig); + }); + queryClient.setQueryData(queryKey, iChatMessage); + } } ); diff --git a/web/src/context/Chats/NewChatProvider/NewChatProvider.tsx b/web/src/context/Chats/NewChatProvider/NewChatProvider.tsx index ddd872817..abdbfc444 100644 --- a/web/src/context/Chats/NewChatProvider/NewChatProvider.tsx +++ b/web/src/context/Chats/NewChatProvider/NewChatProvider.tsx @@ -107,6 +107,10 @@ export const useBusterNewChat = () => { const onFollowUpChat = useMemoizedFn( async ({ prompt, chatId }: { prompt: string; chatId: string }) => { + busterSocket.once({ + route: '/chats/post:initializeChat', + callback: initializeNewChatCallback + }); await busterSocket.emitAndOnce({ emitEvent: { route: '/chats/post', diff --git a/web/src/context/Chats/NewChatProvider/chatStreamMessageHelper.ts b/web/src/context/Chats/NewChatProvider/chatStreamMessageHelper.ts index 13540dead..0432a5854 100644 --- a/web/src/context/Chats/NewChatProvider/chatStreamMessageHelper.ts +++ b/web/src/context/Chats/NewChatProvider/chatStreamMessageHelper.ts @@ -191,7 +191,7 @@ export const updateReasoningMessage = ( Object.assign(fileContentDraft, existingFile?.file || {}); fileContentDraft.text = newFile.file.text_chunk ? (existingFile?.file?.text || '') + newFile.file.text_chunk - : (newFile.file.text ?? existingFile?.file?.text); + : (existingFile?.file?.text ?? newFile.file.text); //we are going to ignore newfile text in favor of existing... this is because Dallin is having a tough time keep yaml in order fileContentDraft.modified = newFile.file.modified ?? existingFile?.file?.modified; }); diff --git a/web/src/context/Chats/NewChatProvider/useChatStreamMessage.ts b/web/src/context/Chats/NewChatProvider/useChatStreamMessage.ts index e4b8687e4..fe68d1340 100644 --- a/web/src/context/Chats/NewChatProvider/useChatStreamMessage.ts +++ b/web/src/context/Chats/NewChatProvider/useChatStreamMessage.ts @@ -37,10 +37,12 @@ export const useChatStreamMessage = () => { const onUpdateChatMessageTransition = useMemoizedFn( (chatMessage: Parameters[0]) => { - const currentChatMessage = chatRefMessages.current[chatMessage.id]; + const currentChatMessage = chatRefMessages.current[chatMessage.id] || {}; const iChatMessage: IBusterChatMessage = create(currentChatMessage, (draft) => { Object.assign(draft || {}, chatMessage); - if (chatMessage.id) draft.id = chatMessage.id; + if (chatMessage.id) { + draft.id = chatMessage.id; + } })!; chatRefMessages.current[chatMessage.id] = iChatMessage; @@ -79,14 +81,17 @@ export const useChatStreamMessage = () => { }); const initializeNewChatCallback = useMemoizedFn((d: BusterChat) => { + const hasMultipleMessages = d.message_ids.length > 1; const { iChat, iChatMessages } = updateChatToIChat(d, true); chatRef.current[iChat.id] = iChat; normalizeChatMessage(iChatMessages); onUpdateChat(iChat); - onChangePage({ - route: BusterRoutes.APP_CHAT_ID, - chatId: iChat.id - }); + if (!hasMultipleMessages) { + onChangePage({ + route: BusterRoutes.APP_CHAT_ID, + chatId: iChat.id + }); + } }); const replaceMessageCallback = useMemoizedFn( @@ -106,9 +111,12 @@ export const useChatStreamMessage = () => { const _generatingTitleCallback = useMemoizedFn((_: null, newData: ChatEvent_GeneratingTitle) => { const { chat_id } = newData; - const updatedChat = updateChatTitle(chatRef.current[chat_id], newData); - chatRef.current[chat_id] = updatedChat; - onUpdateChat(updatedChat); + const currentChat = chatRef.current[chat_id]; + if (currentChat) { + const updatedChat = updateChatTitle(currentChat, newData); + chatRef.current[chat_id] = updatedChat; + onUpdateChat(updatedChat); + } }); const _generatingResponseMessageCallback = useMemoizedFn( diff --git a/web/src/controllers/ReasoningController/ReasoningController.tsx b/web/src/controllers/ReasoningController/ReasoningController.tsx index aeda7e2c5..25ecb9697 100644 --- a/web/src/controllers/ReasoningController/ReasoningController.tsx +++ b/web/src/controllers/ReasoningController/ReasoningController.tsx @@ -1,10 +1,11 @@ 'use client'; import React from 'react'; -import { useChatIndividualContextSelector } from '@/layouts/ChatLayout/ChatContext'; import { useMessageIndividual } from '@/context/Chats'; import { ReasoningMessageSelector } from './ReasoningMessages'; import { BlackBoxMessage } from './ReasoningMessages/ReasoningBlackBoxMessage'; +import { useGetChat } from '@/api/buster_rest/chats'; +import { FileIndeterminateLoader } from '@/components/features/FileIndeterminateLoader'; interface ReasoningControllerProps { chatId: string; @@ -12,12 +13,12 @@ interface ReasoningControllerProps { } export const ReasoningController: React.FC = ({ chatId, messageId }) => { - const hasChat = useChatIndividualContextSelector((state) => state.hasChat); + const { data: hasChat } = useGetChat({ id: chatId || '' }, (x) => !!x.id); + const reasoningMessageIds = useMessageIndividual(messageId, (x) => x?.reasoning_message_ids); const isCompletedStream = useMessageIndividual(messageId, (x) => x?.isCompletedStream); - if (!hasChat || !reasoningMessageIds) - return <>ReasoningController: If you are seeing this there is probably an error...; + if (!hasChat || !reasoningMessageIds) return ; return (
    diff --git a/web/src/controllers/ReasoningController/ReasoningMessages/BarContainer.tsx b/web/src/controllers/ReasoningController/ReasoningMessages/BarContainer.tsx index e7d4e1936..dc09164ec 100644 --- a/web/src/controllers/ReasoningController/ReasoningMessages/BarContainer.tsx +++ b/web/src/controllers/ReasoningController/ReasoningMessages/BarContainer.tsx @@ -23,7 +23,11 @@ export const BarContainer: React.FC<{ />
    - + {children}
    @@ -77,12 +81,14 @@ VerticalBar.displayName = 'VerticalBar'; const TitleContainer: React.FC<{ title: string; secondaryTitle?: string; -}> = React.memo(({ title, secondaryTitle }) => { + isCompletedStream: boolean; +}> = React.memo(({ title, secondaryTitle, isCompletedStream }) => { return (
    - + @@ -96,22 +102,24 @@ const AnimatedThoughtTitle = React.memo( ({ title, type, + isCompletedStream, className = '' }: { title: string | undefined; type: 'tertiary' | 'default'; className?: string; + isCompletedStream: boolean; }) => { const isSecondaryTitle = type === 'tertiary'; return ( - + {title && ( = isCompletedStream={isCompletedStream} title={title ?? ''} secondaryTitle={secondary_title ?? ''}> - +
    = React.memo( ({ reasoningMessageId, messageId, isCompletedStream }) => { @@ -12,7 +11,14 @@ export const ReasoningMessage_Text: React.FC = React.memo (x) => (x?.reasoning_messages[reasoningMessageId] as BusterChatMessageReasoning_text)?.message )!; - return ; + return ( + + ); } ); diff --git a/web/src/layouts/ChatLayout/ChatContainer/ChatContainer.tsx b/web/src/layouts/ChatLayout/ChatContainer/ChatContainer.tsx index 77687b591..f83a7e8b2 100644 --- a/web/src/layouts/ChatLayout/ChatContainer/ChatContainer.tsx +++ b/web/src/layouts/ChatLayout/ChatContainer/ChatContainer.tsx @@ -16,7 +16,7 @@ export const ChatContainer = React.memo(() => { return ( } + header={} headerBorderVariant="ghost" className="flex h-full w-full min-w-[295px] flex-col"> diff --git a/web/src/layouts/ChatLayout/ChatContainer/ChatContent/ChatResponseMessages/ChatResponseMessages.tsx b/web/src/layouts/ChatLayout/ChatContainer/ChatContent/ChatResponseMessages/ChatResponseMessages.tsx index 977d90e40..0a169a92f 100644 --- a/web/src/layouts/ChatLayout/ChatContainer/ChatContent/ChatResponseMessages/ChatResponseMessages.tsx +++ b/web/src/layouts/ChatLayout/ChatContainer/ChatContent/ChatResponseMessages/ChatResponseMessages.tsx @@ -2,7 +2,6 @@ import React from 'react'; import { MessageContainer } from '../MessageContainer'; import { ChatResponseMessageSelector } from './ChatResponseMessageSelector'; import { ChatResponseReasoning } from './ChatResponseReasoning'; -import { ShimmerText } from '@/components/ui/typography/ShimmerText'; import { useMessageIndividual } from '@/context/Chats'; interface ChatResponseMessagesProps { diff --git a/web/src/layouts/ChatLayout/ChatContainer/ChatContent/ChatResponseMessages/ChatResponseReasoning.tsx b/web/src/layouts/ChatLayout/ChatContainer/ChatContent/ChatResponseMessages/ChatResponseReasoning.tsx index bec7711ef..a5d546f58 100644 --- a/web/src/layouts/ChatLayout/ChatContainer/ChatContent/ChatResponseMessages/ChatResponseReasoning.tsx +++ b/web/src/layouts/ChatLayout/ChatContainer/ChatContent/ChatResponseMessages/ChatResponseReasoning.tsx @@ -55,10 +55,12 @@ export const ChatResponseReasoning: React.FC<{ {isReasonginFileSelected ? ( - {text} + + {text} + ) : ( )} diff --git a/web/src/layouts/ChatLayout/ChatContainer/ChatContent/ChatUserMessage.tsx b/web/src/layouts/ChatLayout/ChatContainer/ChatContent/ChatUserMessage.tsx index 6e6b330d8..d00066bda 100644 --- a/web/src/layouts/ChatLayout/ChatContainer/ChatContent/ChatUserMessage.tsx +++ b/web/src/layouts/ChatLayout/ChatContainer/ChatContent/ChatUserMessage.tsx @@ -11,7 +11,7 @@ export const ChatUserMessage: React.FC<{ requestMessage: BusterChatMessageReques return ( - {request} + {request} ); } diff --git a/web/src/layouts/ChatLayout/ChatContainer/ChatHeader/ChatHeader.tsx b/web/src/layouts/ChatLayout/ChatContainer/ChatHeader/ChatHeader.tsx index 3b23d74aa..46096e745 100644 --- a/web/src/layouts/ChatLayout/ChatContainer/ChatHeader/ChatHeader.tsx +++ b/web/src/layouts/ChatLayout/ChatContainer/ChatHeader/ChatHeader.tsx @@ -5,17 +5,15 @@ import { ChatHeaderOptions } from './ChatHeaderOptions'; import { ChatHeaderTitle } from './ChatHeaderTitle'; import { useChatIndividualContextSelector } from '../../ChatContext'; -export const ChatHeader: React.FC<{ - showScrollOverflow: boolean; -}> = React.memo(({ showScrollOverflow }) => { +export const ChatHeader: React.FC<{}> = React.memo(({}) => { const hasFile = useChatIndividualContextSelector((state) => state.hasFile); const chatTitle = useChatIndividualContextSelector((state) => state.chatTitle); - if (!hasFile && !chatTitle) return null; + if (!hasFile || !chatTitle) return null; return ( <> - + ); diff --git a/web/src/layouts/ChatLayout/ChatContainer/ChatHeader/ChatHeaderTitle.tsx b/web/src/layouts/ChatLayout/ChatContainer/ChatHeader/ChatHeaderTitle.tsx index 804a09cd7..dffb539b6 100644 --- a/web/src/layouts/ChatLayout/ChatContainer/ChatHeader/ChatHeaderTitle.tsx +++ b/web/src/layouts/ChatLayout/ChatContainer/ChatHeader/ChatHeaderTitle.tsx @@ -3,7 +3,6 @@ import { Text } from '@/components/ui/typography'; import React from 'react'; import { AnimatePresence, motion } from 'framer-motion'; -import { useChatIndividualContextSelector } from '../../ChatContext'; const animation = { initial: { opacity: 0 }, @@ -12,8 +11,10 @@ const animation = { transition: { duration: 0.25 } }; -export const ChatHeaderTitle: React.FC<{}> = React.memo(() => { - const chatTitle = useChatIndividualContextSelector((state) => state.chatTitle); +export const ChatHeaderTitle: React.FC<{ + chatTitle: string; +}> = React.memo(({ chatTitle }) => { + if (!chatTitle) return
    ; return ( diff --git a/web/src/layouts/ChatLayout/ChatContext/ChatContext.tsx b/web/src/layouts/ChatLayout/ChatContext/ChatContext.tsx index f27e3cb60..b4ca43148 100644 --- a/web/src/layouts/ChatLayout/ChatContext/ChatContext.tsx +++ b/web/src/layouts/ChatLayout/ChatContext/ChatContext.tsx @@ -20,8 +20,12 @@ const useChatIndividualContext = ({ const selectedFileType = selectedFile?.type; //CHAT - const { data: chat } = useGetChat({ id: chatId || '' }); - const hasChat = !!chatId && !!chat; + const { data: chat } = useGetChat({ id: chatId || '' }, (x) => ({ + title: x.title, + message_ids: x.message_ids, + id: x.id + })); + const hasChat = !!chatId && !!chat?.id; const chatTitle = chat?.title; const chatMessageIds = chat?.message_ids ?? []; diff --git a/web/src/layouts/ChatLayout/ChatContext/useAutoChangeLayout.ts b/web/src/layouts/ChatLayout/ChatContext/useAutoChangeLayout.ts index f9f451c5b..ecb7d4eea 100644 --- a/web/src/layouts/ChatLayout/ChatContext/useAutoChangeLayout.ts +++ b/web/src/layouts/ChatLayout/ChatContext/useAutoChangeLayout.ts @@ -11,7 +11,7 @@ export const useAutoChangeLayout = ({ lastMessageId: string; onSetSelectedFile: (file: SelectedFile) => void; }) => { - const hasSeeningReasoningPage = useRef(false); //used when there is a delay in page load + const previousLastMessageId = useRef(null); const reasoningMessagesLength = useMessageIndividual( lastMessageId, (x) => x?.reasoning_message_ids?.length || 0 @@ -21,9 +21,10 @@ export const useAutoChangeLayout = ({ //change the page to reasoning file if we get a reasoning message useEffect(() => { - if (!isCompletedStream && !hasSeeningReasoningPage.current && hasReasoning) { - hasSeeningReasoningPage.current = true; + if (!isCompletedStream && hasReasoning && previousLastMessageId.current !== lastMessageId) { + // hasSeeningReasoningPage.current = true; onSetSelectedFile({ id: lastMessageId, type: 'reasoning' }); + previousLastMessageId.current = lastMessageId; } - }, [isCompletedStream, hasReasoning]); + }, [isCompletedStream, hasReasoning, lastMessageId]); }; diff --git a/web/src/lib/chat.ts b/web/src/lib/chat.ts index b44f9b9f8..07e50d238 100644 --- a/web/src/lib/chat.ts +++ b/web/src/lib/chat.ts @@ -4,6 +4,7 @@ import { create } from 'mutative'; import omit from 'lodash/omit'; import { BusterMetric, IBusterMetric } from '@/api/asset_interfaces/metric'; import { createDefaultChartConfig } from './messageAutoChartHandler'; +import last from 'lodash/last'; const chatUpgrader = (chat: BusterChat, { isNewChat }: { isNewChat: boolean }): IBusterChat => { return { @@ -20,7 +21,7 @@ const chatMessageUpgrader = ( return messageIds.reduce( (acc, messageId) => { acc[messageId] = create(message[messageId] as IBusterChatMessage, (draft) => { - draft.isCompletedStream = streamingMessageId !== messageId; + draft.isCompletedStream = !streamingMessageId || streamingMessageId !== messageId; return draft; }); return acc; @@ -37,7 +38,7 @@ export const updateChatToIChat = ( const iChatMessages = chatMessageUpgrader( chat.message_ids, chat.messages, - isNewChat ? chat.message_ids[0] : undefined + isNewChat ? last(chat.message_ids) : undefined ); return { iChat, diff --git a/web/src/styles/buster.scss b/web/src/styles/buster.scss index f7f28847c..cfa941976 100644 --- a/web/src/styles/buster.scss +++ b/web/src/styles/buster.scss @@ -5,3 +5,31 @@ display: none; } } + +// This approach has very limited browser support +:root { + --globalScrollTimeline: none; // Define the timeline globally +} + +.scroll-shadow-container { + position: relative; + scroll-timeline-name: --globalScrollTimeline; + @apply relative; + + .scroll-header { + @apply fixed top-[30px] right-0 left-0 h-2 w-full; + animation: shadowAnimation linear; + animation-range: 0px 125px; + animation-timeline: --globalScrollTimeline; + animation-fill-mode: forwards; + } + + @keyframes shadowAnimation { + from { + box-shadow: 0 0 0 rgba(0, 0, 0, 0); + } + to { + box-shadow: 0px 1px 8px 0px #00000029; + } + } +} diff --git a/web/src/styles/styles.scss b/web/src/styles/styles.scss index 4eb89a80c..d2ec17221 100644 --- a/web/src/styles/styles.scss +++ b/web/src/styles/styles.scss @@ -45,5 +45,5 @@ body { } p { - @apply leading-1.3 text-base; + @apply leading-1.3; } diff --git a/web/src/styles/tailwind.css b/web/src/styles/tailwind.css index b8a989a0e..f8211c5ad 100644 --- a/web/src/styles/tailwind.css +++ b/web/src/styles/tailwind.css @@ -40,6 +40,7 @@ --text-3xl--line-height: 1; --text-4xl: 30px; --text-4xl--line-height: 1; + --text-size-inherit: inherit; --text-icon-size: 16px; --text-icon-size--line-height: 1.05;