diff --git a/web/src/api/asset_interfaces/chat/chatMessageInterfaces.ts b/web/src/api/asset_interfaces/chat/chatMessageInterfaces.ts index 96fd17112..3c1bcc73d 100644 --- a/web/src/api/asset_interfaces/chat/chatMessageInterfaces.ts +++ b/web/src/api/asset_interfaces/chat/chatMessageInterfaces.ts @@ -3,7 +3,7 @@ import type { FileType, ThoughtFileType } from './config'; export type BusterChatMessage = { id: string; - request_message: BusterChatMessageRequest; + request_message: BusterChatMessageRequest | null; response_message_ids: string[]; response_messages: Record; reasoning_message_ids: string[]; diff --git a/web/src/api/buster_rest/chats/queryRequests.ts b/web/src/api/buster_rest/chats/queryRequests.ts index b23796dc9..2c1265c6d 100644 --- a/web/src/api/buster_rest/chats/queryRequests.ts +++ b/web/src/api/buster_rest/chats/queryRequests.ts @@ -153,7 +153,7 @@ export const useDeleteChat = () => { }); }; -export const useGetChatMemoized = () => { +export const useGetChatMessageMemoized = () => { const queryClient = useQueryClient(); const getChatMessageMemoized = useMemoizedFn((messageId: string) => { @@ -165,6 +165,18 @@ export const useGetChatMemoized = () => { return getChatMessageMemoized; }; +export const useGetChatMemoized = () => { + const queryClient = useQueryClient(); + + const getChatMemoized = useMemoizedFn((chatId: string) => { + const options = queryKeys.chatsGetChat(chatId); + const queryKey = options.queryKey; + return queryClient.getQueryData(queryKey); + }); + + return getChatMemoized; +}; + export const useGetChatMessage = ( messageId: string, selector?: (message: IBusterChatMessage) => TData diff --git a/web/src/context/Chats/NewChatProvider.tsx b/web/src/context/Chats/NewChatProvider.tsx index abdbfc444..2ee501640 100644 --- a/web/src/context/Chats/NewChatProvider.tsx +++ b/web/src/context/Chats/NewChatProvider.tsx @@ -6,16 +6,18 @@ import { useMemoizedFn } from '@/hooks'; import type { BusterSearchResult, FileType } from '@/api/asset_interfaces'; import { useBusterWebSocket } from '@/context/BusterWebSocket'; import { useChatStreamMessage } from './useChatStreamMessage'; +import { useGetChatMemoized, useGetChatMessageMemoized } from '@/api/buster_rest/chats'; +import { useChatUpdate } from './useChatUpdate'; +import { create } from 'mutative'; export const useBusterNewChat = () => { const busterSocket = useBusterWebSocket(); + const getChatMessageMemoized = useGetChatMessageMemoized(); + const getChatMemoized = useGetChatMemoized(); + const { onUpdateChat, onUpdateChatMessage } = useChatUpdate(); - const { - completeChatCallback, - stopChatCallback, - initializeNewChatCallback, - replaceMessageCallback - } = useChatStreamMessage(); + const { completeChatCallback, stopChatCallback, initializeNewChatCallback } = + useChatStreamMessage(); const onSelectSearchAsset = useMemoizedFn(async (asset: BusterSearchResult) => { await new Promise((resolve) => setTimeout(resolve, 1000)); @@ -84,10 +86,31 @@ export const useBusterNewChat = () => { messageId: string; chatId: string; }) => { - replaceMessageCallback({ - prompt, - messageId + const currentChat = getChatMemoized(chatId); + const currentMessage = getChatMessageMemoized(messageId); + const currentRequestMessage = currentMessage?.request_message!; + onUpdateChatMessage({ + id: messageId, + request_message: create(currentRequestMessage, (draft) => { + draft.request = prompt; + }), + reasoning_message_ids: [], + response_message_ids: [], + isCompletedStream: false }); + + const messageIndex = currentChat?.message_ids.findIndex( + (messageId) => messageId === messageId + ); + + if (messageIndex && messageIndex !== -1) { + const updatedMessageIds = currentChat?.message_ids.slice(0, messageIndex + 1); + onUpdateChat({ + id: chatId, + message_ids: updatedMessageIds + }); + } + await busterSocket.emitAndOnce({ emitEvent: { route: '/chats/post', diff --git a/web/src/context/Chats/useBlackBoxMessage.ts b/web/src/context/Chats/useBlackBoxMessage.ts index a13fb5bdb..84766f7d4 100644 --- a/web/src/context/Chats/useBlackBoxMessage.ts +++ b/web/src/context/Chats/useBlackBoxMessage.ts @@ -9,11 +9,11 @@ import { IBusterChatMessage } from '@/api/asset_interfaces/chat'; import { ChatEvent_GeneratingReasoningMessage } from '@/api/buster_socket/chats'; import { useQueryClient } from '@tanstack/react-query'; import { queryKeys } from '@/api/query_keys'; -import { useGetChatMemoized } from '@/api/buster_rest/chats'; +import { useGetChatMessageMemoized } from '@/api/buster_rest/chats'; export const useBlackBoxMessage = () => { const timeoutRef = useRef>>({}); - const getChatMessageMemoized = useGetChatMemoized(); + const getChatMessageMemoized = useGetChatMessageMemoized(); const queryClient = useQueryClient(); const clearTimeoutRef = useMemoizedFn((messageId: string) => { diff --git a/web/src/context/Chats/useChatStreamMessage.ts b/web/src/context/Chats/useChatStreamMessage.ts index c1d23c003..6c9453438 100644 --- a/web/src/context/Chats/useChatStreamMessage.ts +++ b/web/src/context/Chats/useChatStreamMessage.ts @@ -21,14 +21,12 @@ import { updateResponseMessage, updateReasoningMessage } from './chatStreamMessageHelper'; -import { useGetChatMemoized } from '@/api/buster_rest/chats'; import { useChatUpdate } from './useChatUpdate'; -import { prefetchGetMetricDataClient, prefetchGetMetric } from '@/api/buster_rest/metrics'; +import { prefetchGetMetricDataClient } from '@/api/buster_rest/metrics'; export const useChatStreamMessage = () => { const queryClient = useQueryClient(); const onChangePage = useAppLayoutContextSelector((x) => x.onChangePage); - const getChatMessageMemoized = useGetChatMemoized(); const { onUpdateChat, onUpdateChatMessage } = useChatUpdate(); const chatRef = useRef>({}); const chatRefMessages = useRef>({}); @@ -110,21 +108,6 @@ export const useChatStreamMessage = () => { } }); - const replaceMessageCallback = useMemoizedFn( - ({ prompt, messageId }: { prompt: string; messageId: string }) => { - const currentMessage = getChatMessageMemoized(messageId); - const currentRequestMessage = currentMessage?.request_message!; - onUpdateChatMessage({ - id: messageId, - request_message: create(currentRequestMessage, (draft) => { - draft.request = prompt; - }), - reasoning_message_ids: [], - response_message_ids: [] - }); - } - ); - const _generatingTitleCallback = useMemoizedFn((_: null, newData: ChatEvent_GeneratingTitle) => { const { chat_id } = newData; const currentChat = chatRef.current[chat_id]; @@ -189,7 +172,6 @@ export const useChatStreamMessage = () => { return { initializeNewChatCallback, completeChatCallback, - stopChatCallback, - replaceMessageCallback + stopChatCallback }; }; diff --git a/web/src/layouts/ChatLayout/ChatContainer/ChatContent/ChatMessageBlock.tsx b/web/src/layouts/ChatLayout/ChatContainer/ChatContent/ChatMessageBlock.tsx index 5ea829975..1810e207f 100644 --- a/web/src/layouts/ChatLayout/ChatContainer/ChatContent/ChatMessageBlock.tsx +++ b/web/src/layouts/ChatLayout/ChatContainer/ChatContent/ChatMessageBlock.tsx @@ -7,14 +7,22 @@ export const ChatMessageBlock: React.FC<{ messageId: string; chatId: string; }> = React.memo(({ messageId, chatId }) => { + const messageExists = useGetChatMessage(messageId, (message) => message?.id); const requestMessage = useGetChatMessage(messageId, (message) => message?.request_message); const isCompletedStream = useGetChatMessage(messageId, (x) => x?.isCompletedStream); - if (!requestMessage) return null; + if (!messageExists) return null; return (
- + {requestMessage && ( + + )} = React.memo( - ({ requestMessage }) => { - if (!requestMessage) return null; +export const ChatUserMessage: React.FC<{ + messageId: string; + chatId: string; + isCompletedStream: boolean; + requestMessage: NonNullable; +}> = React.memo(({ messageId, chatId, isCompletedStream, requestMessage }) => { + const [isTooltipOpen, setIsTooltipOpen] = useState(false); + const [isEditing, setIsEditing] = useState(false); - const { sender_avatar, sender_id, sender_name, request } = requestMessage; + const { sender_avatar, sender_id, sender_name, request } = requestMessage; - return ( - - {request} - - ); - } -); + const onSetIsEditing = useMemoizedFn((isEditing: boolean) => { + setIsEditing(isEditing); + setIsTooltipOpen(false); + }); + + return ( + setIsTooltipOpen(true)} + onMouseLeave={() => setIsTooltipOpen(false)}> + {isEditing ? ( + + ) : ( + <> + {request} + {isCompletedStream && ( + + )} + + )} + + ); +}); ChatUserMessage.displayName = 'ChatUserMessage'; + +const RequestMessageTooltip: React.FC<{ + isTooltipOpen: boolean; + requestMessage: NonNullable; + setIsEditing: (isEditing: boolean) => void; +}> = React.memo(({ isTooltipOpen, requestMessage, setIsEditing }) => { + const { openSuccessMessage } = useBusterNotifications(); + + const onCopy = useMemoizedFn(() => { + navigator.clipboard.writeText(requestMessage.request); + openSuccessMessage('Copied to clipboard'); + }); + + const onEdit = useMemoizedFn(() => { + setIsEditing(true); + }); + + return ( +
+ +
+ ); +}); + +RequestMessageTooltip.displayName = 'RequestMessageTooltip'; + +const EditMessage: React.FC<{ + requestMessage: NonNullable; + onSetIsEditing: (isEditing: boolean) => void; + messageId: string; + chatId: string; +}> = React.memo(({ requestMessage, onSetIsEditing, messageId, chatId }) => { + const [prompt, setPrompt] = useState(requestMessage.request); + const onReplaceMessageInChat = useBusterNewChatContextSelector((x) => x.onReplaceMessageInChat); + + const onSave = useMemoizedFn((text: string) => { + onReplaceMessageInChat({ + chatId, + messageId, + prompt + }); + onSetIsEditing(false); + }); + + return ( +
+ setPrompt(e.target.value)} + /> +
+ + +
+
+ ); +}); diff --git a/web/src/layouts/ChatLayout/ChatContainer/ChatContent/MessageContainer.tsx b/web/src/layouts/ChatLayout/ChatContainer/ChatContent/MessageContainer.tsx index 43d0b44df..4d0e5a999 100644 --- a/web/src/layouts/ChatLayout/ChatContainer/ChatContent/MessageContainer.tsx +++ b/web/src/layouts/ChatLayout/ChatContainer/ChatContent/MessageContainer.tsx @@ -1,24 +1,37 @@ import { Avatar } from '@/components/ui/avatar'; import { cn } from '@/lib/classMerge'; -import React from 'react'; +import React, { forwardRef } from 'react'; -export const MessageContainer: React.FC<{ +interface MessageContainerProps { children: React.ReactNode; senderName?: string; senderId?: string; senderAvatar?: string | null; className?: string; -}> = React.memo(({ children, senderName, senderId, senderAvatar, className = '' }) => { - return ( -
- {senderName ? ( - - ) : ( - - )} -
{children}
-
- ); -}); + onMouseEnter?: () => void; + onMouseLeave?: () => void; +} + +export const MessageContainer = forwardRef( + ( + { children, senderName, senderId, senderAvatar, className = '', onMouseEnter, onMouseLeave }, + ref + ) => { + return ( +
+ {senderName ? ( + + ) : ( + + )} +
{children}
+
+ ); + } +); MessageContainer.displayName = 'MessageContainer'; diff --git a/web/src/layouts/ChatLayout/ChatContext/useAutoChangeLayout.ts b/web/src/layouts/ChatLayout/ChatContext/useAutoChangeLayout.ts index 4328e8ae5..2c731e801 100644 --- a/web/src/layouts/ChatLayout/ChatContext/useAutoChangeLayout.ts +++ b/web/src/layouts/ChatLayout/ChatContext/useAutoChangeLayout.ts @@ -1,12 +1,10 @@ 'use client'; -import { useGetChatMemoized, useGetChatMessage } from '@/api/buster_rest/chats'; +import { useGetChatMessageMemoized, useGetChatMessage } from '@/api/buster_rest/chats'; import type { SelectedFile } from '../interfaces'; import { useEffect, useRef } from 'react'; import findLast from 'lodash/findLast'; import { BusterChatResponseMessage_file } from '@/api/asset_interfaces/chat'; -import { useAppLayoutContextSelector } from '@/context/BusterAppLayout'; -import { BusterRoutes } from '@/routes'; export const useAutoChangeLayout = ({ lastMessageId, @@ -24,7 +22,7 @@ export const useAutoChangeLayout = ({ lastMessageId, (x) => x?.reasoning_message_ids?.length || 0 ); - const getChatMessageMemoized = useGetChatMemoized(); + const getChatMessageMemoized = useGetChatMessageMemoized(); const isCompletedStream = useGetChatMessage(lastMessageId, (x) => x?.isCompletedStream); const hasReasoning = !!reasoningMessagesLength;