diff --git a/web/src/app/app/_layouts/ChatLayout/ChatContainer/ChatContent/ChatMessageBlock.tsx b/web/src/app/app/_layouts/ChatLayout/ChatContainer/ChatContent/ChatMessageBlock.tsx index d21cb36c1..18b035f0e 100644 --- a/web/src/app/app/_layouts/ChatLayout/ChatContainer/ChatContent/ChatMessageBlock.tsx +++ b/web/src/app/app/_layouts/ChatLayout/ChatContainer/ChatContent/ChatMessageBlock.tsx @@ -6,7 +6,7 @@ import type { IBusterChatMessage } from '@/context/Chats/interfaces'; export const ChatMessageBlock: React.FC<{ message: IBusterChatMessage; }> = React.memo(({ message }) => { - const { request_message, response_messages, id, isCompletedStream } = message; + const { request_message, response_messages, id, isCompletedStream, reasoning } = message; return (
@@ -14,6 +14,8 @@ export const ChatMessageBlock: React.FC<{
); diff --git a/web/src/app/app/_layouts/ChatLayout/ChatContainer/ChatContent/ChatResponseMessages/ChatResponseMessageSelector.tsx b/web/src/app/app/_layouts/ChatLayout/ChatContainer/ChatContent/ChatResponseMessages/ChatResponseMessageSelector.tsx index 48573c611..8881ea0cc 100644 --- a/web/src/app/app/_layouts/ChatLayout/ChatContainer/ChatContent/ChatResponseMessages/ChatResponseMessageSelector.tsx +++ b/web/src/app/app/_layouts/ChatLayout/ChatContainer/ChatContent/ChatResponseMessages/ChatResponseMessageSelector.tsx @@ -1,7 +1,9 @@ -import React from 'react'; +import React, { useMemo } from 'react'; import { ChatResponseMessage_File } from './ChatResponseMessage_File'; import { StreamingMessage_Text } from '@appComponents/Streaming/StreamingMessage_Text'; import type { BusterChatMessage_text, BusterChatMessageResponse } from '@/api/asset_interfaces'; +import { createStyles } from 'antd-style'; +import { useMemoizedFn } from 'ahooks'; export interface ChatResponseMessageProps { responseMessage: BusterChatMessageResponse; @@ -32,11 +34,74 @@ export const ChatResponseMessageSelector: React.FC { const messageType = responseMessage.type; const ChatResponseMessage = ChatResponseMessageRecord[messageType]; + const { cx, styles } = useStyles(); + + const typeClassRecord: Record = useMemo(() => { + return { + text: cx(styles.textCard, 'text-card'), + file: cx(styles.fileCard, 'file-card') + }; + }, []); + + const getContainerClass = useMemoizedFn((item: BusterChatMessageResponse) => { + return typeClassRecord[item.type]; + }); + return ( - +
+ + +
); }; + +const VerticalDivider: React.FC<{ className?: string }> = React.memo(({ className }) => { + const { cx, styles } = useStyles(); + return
; +}); +VerticalDivider.displayName = 'VerticalDivider'; + +const useStyles = createStyles(({ token, css }) => ({ + textCard: css` + margin-bottom: 14px; + + &:has(+ .text-card) { + margin-bottom: 8px; + } + + .vertical-divider { + display: none; + } + `, + fileCard: css` + &:has(+ .text-card) { + .vertical-divider { + opacity: 0; + } + } + + &:has(+ .file-card) { + .vertical-divider { + opacity: 1; + } + margin-bottom: 1px; + } + + &:last-child { + .vertical-divider { + opacity: 0; + } + } + `, + verticalDivider: css` + transition: opacity 0.2s ease-in-out; + height: 9px; + width: 0.5px; + margin: 3px 0 3px 16px; + background: ${token.colorTextTertiary}; + ` +})); diff --git a/web/src/app/app/_layouts/ChatLayout/ChatContainer/ChatContent/ChatResponseMessages/ChatResponseMessages.tsx b/web/src/app/app/_layouts/ChatLayout/ChatContainer/ChatContent/ChatResponseMessages/ChatResponseMessages.tsx index 4d67005a8..7a8467bfe 100644 --- a/web/src/app/app/_layouts/ChatLayout/ChatContainer/ChatContent/ChatResponseMessages/ChatResponseMessages.tsx +++ b/web/src/app/app/_layouts/ChatLayout/ChatContainer/ChatContent/ChatResponseMessages/ChatResponseMessages.tsx @@ -1,46 +1,56 @@ import React, { useMemo } from 'react'; -import type { BusterChatMessageResponse } from '@/api/asset_interfaces'; +import type { + BusterChatMessage_text, + BusterChatMessageReasoning, + BusterChatMessageResponse +} from '@/api/asset_interfaces'; import { MessageContainer } from '../MessageContainer'; -import { useMemoizedFn } from 'ahooks'; import { ChatResponseMessageSelector } from './ChatResponseMessageSelector'; -import { createStyles } from 'antd-style'; +import { ChatResponseReasoning } from './ChatResponseReasoning'; interface ChatResponseMessagesProps { responseMessages: BusterChatMessageResponse[]; isCompletedStream: boolean; + reasoningMessages: BusterChatMessageReasoning[]; + messageId: string; } export const ChatResponseMessages: React.FC = React.memo( - ({ responseMessages, isCompletedStream }) => { - const { styles, cx } = useStyles(); - - const firstResponseMessage = responseMessages[0]; - const restResponseMessages = responseMessages.slice(1); + ({ responseMessages, reasoningMessages, isCompletedStream, messageId }) => { + const firstResponseMessage = responseMessages[0] as BusterChatMessage_text; + const restResponseMessages = useMemo(() => { + if (!firstResponseMessage) return []; + return responseMessages.slice(1); + }, [firstResponseMessage, responseMessages]); const lastMessageIndex = responseMessages.length - 1; - const typeClassRecord: Record = useMemo(() => { - return { - text: cx(styles.textCard, 'text-card'), - file: cx(styles.fileCard, 'file-card') - }; - }, []); - - const getContainerClass = useMemoizedFn((item: BusterChatMessageResponse) => { - return typeClassRecord[item.type]; - }); - return ( - {responseMessages.map((responseMessage, index) => ( -
- - -
+ {firstResponseMessage && ( + + )} + + {firstResponseMessage && ( + + )} + + {restResponseMessages.map((responseMessage, index) => ( + ))}
); @@ -48,52 +58,3 @@ export const ChatResponseMessages: React.FC = React.m ); ChatResponseMessages.displayName = 'ChatResponseMessages'; - -const VerticalDivider: React.FC<{ className?: string }> = React.memo(({ className }) => { - const { cx, styles } = useStyles(); - return
; -}); -VerticalDivider.displayName = 'VerticalDivider'; - -const useStyles = createStyles(({ token, css }) => ({ - textCard: css` - margin-bottom: 14px; - - &:has(+ .text-card) { - margin-bottom: 8px; - } - - .vertical-divider { - display: none; - } - `, - fileCard: css` - &:has(+ .text-card), - &:has(+ .hidden-card) { - .vertical-divider { - opacity: 0; - } - margin-bottom: 0px; - } - - &:has(+ .thought-card) { - .vertical-divider { - opacity: 0; - } - margin-bottom: 0px; - } - - &:last-child { - .vertical-divider { - opacity: 0; - } - } - `, - verticalDivider: css` - transition: opacity 0.2s ease-in-out; - height: 9px; - width: 0.5px; - margin: 3px 0 3px 16px; - background: ${token.colorTextTertiary}; - ` -})); diff --git a/web/src/app/app/_layouts/ChatLayout/ChatContainer/ChatContent/ChatResponseMessages/ChatResponseReasoning.tsx b/web/src/app/app/_layouts/ChatLayout/ChatContainer/ChatContent/ChatResponseMessages/ChatResponseReasoning.tsx new file mode 100644 index 000000000..2488d8427 --- /dev/null +++ b/web/src/app/app/_layouts/ChatLayout/ChatContainer/ChatContent/ChatResponseMessages/ChatResponseReasoning.tsx @@ -0,0 +1,175 @@ +import { BusterChatMessageReasoning } from '@/api/asset_interfaces'; +import React, { useEffect, useMemo, useState } from 'react'; +import last from 'lodash/last'; +import { ShimmerText } from '@/components/text'; +import { useMemoizedFn } from 'ahooks'; +import { motion } from 'framer-motion'; +import { AnimatePresence } from 'framer-motion'; +import { AppMaterialIcons, Text } from '@/components'; +import { createStyles } from 'antd-style'; +import { useChatLayoutContextSelector } from '../../../ChatLayoutContext'; + +export const ChatResponseReasoning: React.FC<{ + reasoningMessages: BusterChatMessageReasoning[]; + isCompletedStream: boolean; + messageId: string; +}> = React.memo(({ reasoningMessages, isCompletedStream, messageId }) => { + const lastMessage = last(reasoningMessages); + const onSetSelectedFile = useChatLayoutContextSelector((x) => x.onSetSelectedFile); + const selectedFileType = useChatLayoutContextSelector((x) => x.selectedFileType); + const isReasonginFileSelected = selectedFileType === 'reasoning'; + + const text = useMemo(() => { + if (!lastMessage) return null; + if (lastMessage.type === 'text') { + return lastMessage.message; + } + return lastMessage.thought_title; + }, [lastMessage]); + + const getRandomThought = useMemoizedFn(() => { + return DEFAULT_THOUGHTS[Math.floor(Math.random() * DEFAULT_THOUGHTS.length)]; + }); + + const onClickReasoning = useMemoizedFn(() => { + onSetSelectedFile({ + type: 'reasoning', + id: messageId + }); + }); + + const [thought, setThought] = useState(text || DEFAULT_THOUGHTS[0]); + + useEffect(() => { + if (!isCompletedStream && !text) { + const randomInterval = Math.floor(Math.random() * 3000) + 1200; + const interval = setTimeout(() => { + setThought(getRandomThought()); + }, randomInterval); + return () => clearTimeout(interval); + } + if (text) { + setThought(text); + } + }, [thought, isCompletedStream, text, getRandomThought]); + + const animations = useMemo(() => { + return { + initial: { opacity: 0 }, + animate: { opacity: 1 }, + exit: { opacity: 0 } + }; + }, []); + + return ( + + + + + + ); +}); + +ChatResponseReasoning.displayName = 'ChatThoughts'; + +const DEFAULT_THOUGHTS = [ + 'Thinking through next steps...', + 'Looking through context...', + 'Reflecting on the instructions...', + 'Analyzing available actions', + 'Reviewing the objective...', + 'Deciding feasible options...', + 'Sorting out some details...', + 'Exploring other possibilities...', + 'Confirming things....', + 'Mapping information across files...', + 'Making a few edits...', + 'Filling out arguments...', + 'Double-checking the logic...', + 'Validating my approach...', + 'Looking at a few edge cases...', + 'Ensuring everything aligns...', + 'Polishing the details...', + 'Making some adjustments...', + 'Writing out arguments...', + 'Mapping trends and patterns...', + 'Re-evaluating this step...', + 'Updating parameters...', + 'Evaluating available data...', + 'Reviewing all parameters...', + 'Processing relevant info...', + 'Aligning with user request...', + 'Gathering necessary details...', + 'Sorting through options...', + 'Editing my system logic...', + 'Cross-checking references...', + 'Validating my approach...', + 'Rewriting operational details...', + 'Mapping new information...', + 'Adjusting priorities & approach...', + 'Revisiting earlier inputs...', + 'Finalizing plan details...' +]; + +const ShimmerTextWithIcon = React.memo( + ({ + text, + isCompletedStream, + isSelected + }: { + text: string; + isCompletedStream: boolean; + isSelected: boolean; + }) => { + const { cx, styles } = useStyles(); + + if (isCompletedStream) { + return ( +
+
+ +
+ {text} +
+ ); + } + + return ( +
+
+ +
+ +
+ ); + } +); +ShimmerTextWithIcon.displayName = 'ShimmerTextWithIcon'; + +const useStyles = createStyles(({ token, css }) => ({ + iconContainerCompleted: css` + color: ${token.colorIcon}; + &:hover { + color: ${token.colorText}; + } + &.is-selected { + color: ${token.colorText}; + } + `, + iconContainer: css` + cursor: pointer; + `, + icon: css` + color: ${token.colorIcon}; + ` +})); diff --git a/web/src/app/app/_layouts/ChatLayout/ChatContainer/ChatContent/ChatThoughts.tsx b/web/src/app/app/_layouts/ChatLayout/ChatContainer/ChatContent/ChatThoughts.tsx deleted file mode 100644 index 2943ccbd5..000000000 --- a/web/src/app/app/_layouts/ChatLayout/ChatContainer/ChatContent/ChatThoughts.tsx +++ /dev/null @@ -1,72 +0,0 @@ -import { BusterChatMessageReasoning } from '@/api/asset_interfaces'; -import React, { useMemo } from 'react'; -import last from 'lodash/last'; -import { ShimmerText } from '@/components/text'; - -export const ChatThoughts: React.FC<{ - reasoningMessages: BusterChatMessageReasoning[]; -}> = React.memo(({ reasoningMessages }) => { - const lastMessage = last(reasoningMessages); - - const lastMessageTest = useMemo(() => { - if (!lastMessage) return null; - if (lastMessage.type === 'text') { - return lastMessage; - } - return lastMessage.thought_title; - }, [lastMessage]); - - const hasLastMessage = !!lastMessage || !!lastMessageTest; - - if (!hasLastMessage) return null; - - return
ChatThoughts
; -}); - -ChatThoughts.displayName = 'ChatThoughts'; - -const DEFAULT_THOUGHTS = [ - 'Thinking through next steps...', - 'Looking through context...', - 'Reflecting on the instructions...', - 'Analyzing available actions', - 'Reviewing the objective...', - 'Deciding feasible options...', - 'Sorting out some details...', - 'Exploring other possibilities...', - 'Confirming things....', - 'Mapping information across files...', - 'Making a few edits...', - 'Filling out arguments...', - 'Double-checking the logic...', - 'Validating my approach...', - 'Looking at a few edge cases...', - 'Ensuring everything aligns...', - 'Polishing the details...', - 'Making some adjustments...', - 'Writing out arguments...', - 'Mapping trends and patterns...', - 'Re-evaluating this step...', - 'Updating parameters...', - 'Evaluating available data...', - 'Reviewing all parameters...', - 'Processing relevant info...', - 'Aligning with user request...', - 'Gathering necessary details...', - 'Sorting through options...', - 'Editing my system logic...', - 'Cross-checking references...', - 'Validating my approach...', - 'Rewriting operational details...', - 'Mapping new information...', - 'Adjusting priorities & approach...', - 'Revisiting earlier inputs...', - 'Finalizing plan details...' -]; - -const RandomThoughts = React.memo(() => { - const randomThought = DEFAULT_THOUGHTS[Math.floor(Math.random() * DEFAULT_THOUGHTS.length)]; - return
{randomThought}
; -}); - -RandomThoughts.displayName = 'RandomThoughts'; diff --git a/web/src/app/app/_layouts/ChatLayout/ChatLayoutContext/ChatLayoutContext.tsx b/web/src/app/app/_layouts/ChatLayout/ChatLayoutContext/ChatLayoutContext.tsx index 75ba48aa6..21dd549c3 100644 --- a/web/src/app/app/_layouts/ChatLayout/ChatLayoutContext/ChatLayoutContext.tsx +++ b/web/src/app/app/_layouts/ChatLayout/ChatLayoutContext/ChatLayoutContext.tsx @@ -3,7 +3,7 @@ import { createContext, useContextSelector } from '@fluentui/react-context-selector'; -import React, { PropsWithChildren, useState, useTransition } from 'react'; +import React, { PropsWithChildren, useTransition } from 'react'; import type { SelectedFile } from '../interfaces'; import type { ChatSplitterProps } from '../ChatLayout'; import { useMemoizedFn } from 'ahooks'; @@ -51,7 +51,7 @@ export const useChatLayout = ({ const fileType = file.type; const fileId = file.id; const route = - isChatView && chatId + isChatView && chatId !== undefined ? createChatAssetRoute({ chatId, assetId: fileId, type: fileType }) : createFileRoute({ assetId: fileId, type: fileType }); diff --git a/web/src/app/app/_layouts/ChatLayout/ChatLayoutContext/publicHelpers.ts b/web/src/app/app/_layouts/ChatLayout/ChatLayoutContext/publicHelpers.ts index 2752c921f..874b4f3f5 100644 --- a/web/src/app/app/_layouts/ChatLayout/ChatLayoutContext/publicHelpers.ts +++ b/web/src/app/app/_layouts/ChatLayout/ChatLayoutContext/publicHelpers.ts @@ -1,6 +1,7 @@ import type { ThoughtFileType, FileType } from '@/api/asset_interfaces'; -export const isOpenableFile = (type: ThoughtFileType): type is FileType => { - const validTypes: FileType[] = ['metric', 'dashboard']; - return validTypes.includes(type as FileType); +const OPENABLE_FILES = new Set(['metric', 'dashboard', 'reasoning']); + +export const isOpenableFile = (type: ThoughtFileType): boolean => { + return OPENABLE_FILES.has(type); }; diff --git a/web/src/app/app/_layouts/ChatLayout/hooks/useDefaultFile.ts b/web/src/app/app/_layouts/ChatLayout/hooks/useDefaultFile.ts index c1b724892..35dbc93cf 100644 --- a/web/src/app/app/_layouts/ChatLayout/hooks/useDefaultFile.ts +++ b/web/src/app/app/_layouts/ChatLayout/hooks/useDefaultFile.ts @@ -36,7 +36,5 @@ export const useSelectedFileByParams = () => { return 'chat'; }, [metricId, collectionId, datasetId, dashboardId, chatId]); - console.log(selectedFile, selectedLayout, chatId); - return { selectedFile, selectedLayout, chatId }; }; diff --git a/web/src/context/Chats/ChatProvider/MOCK_CHAT.ts b/web/src/context/Chats/ChatProvider/MOCK_CHAT.ts index a50bb8991..8c60b563f 100644 --- a/web/src/context/Chats/ChatProvider/MOCK_CHAT.ts +++ b/web/src/context/Chats/ChatProvider/MOCK_CHAT.ts @@ -90,6 +90,7 @@ export const MOCK_CHAT: BusterChat = { response_messages: [ createMockResponseMessageText(), createMockResponseMessageFile(), + createMockResponseMessageFile(), createMockResponseMessageText(), createMockResponseMessageText() ] diff --git a/web/src/context/Chats/ChatProvider/useChatSubscriptions.ts b/web/src/context/Chats/ChatProvider/useChatSubscriptions.ts index 60bee8a07..04956e49a 100644 --- a/web/src/context/Chats/ChatProvider/useChatSubscriptions.ts +++ b/web/src/context/Chats/ChatProvider/useChatSubscriptions.ts @@ -4,7 +4,13 @@ import { useMemoizedFn } from 'ahooks'; import { BusterChat } from '@/api/asset_interfaces'; import { IBusterChat } from '../interfaces'; import { chatUpgrader } from './helpers'; -import { MOCK_CHAT } from './MOCK_CHAT'; +import { + createMockResponseMessageFile, + createMockResponseMessageText, + createMockResponseMessageThought, + MOCK_CHAT +} from './MOCK_CHAT'; +import { useHotkeys } from 'react-hotkeys-hook'; export const useChatSubscriptions = ({ chatsRef, @@ -47,6 +53,73 @@ export const useChatSubscriptions = ({ // }); }); + useHotkeys('t', () => { + const newThoughts = createMockResponseMessageThought(); + const myChat = { + ...chatsRef.current[MOCK_CHAT.id]!, + messages: [ + { + ...chatsRef.current[MOCK_CHAT.id]!.messages[0], + reasoning: [...chatsRef.current[MOCK_CHAT.id]!.messages[0].reasoning, newThoughts], + isCompletedStream: false + } + ] + }; + + chatsRef.current[MOCK_CHAT.id] = myChat; + + startTransition(() => { + // Create a new reference to trigger React update + chatsRef.current = { ...chatsRef.current }; + }); + }); + + useHotkeys('m', () => { + const newTextMessage = createMockResponseMessageText(); + const myChat = { + ...chatsRef.current[MOCK_CHAT.id]!, + messages: [ + { + ...chatsRef.current[MOCK_CHAT.id]!.messages[0], + response_messages: [ + ...chatsRef.current[MOCK_CHAT.id]!.messages[0]!.response_messages, + newTextMessage + ], + isCompletedStream: false + } + ] + }; + + chatsRef.current[MOCK_CHAT.id] = myChat; + + startTransition(() => { + chatsRef.current = { ...chatsRef.current }; + }); + }); + + useHotkeys('f', () => { + const newFileMessage = createMockResponseMessageFile(); + const myChat = { + ...chatsRef.current[MOCK_CHAT.id]!, + messages: [ + { + ...chatsRef.current[MOCK_CHAT.id]!.messages[0], + response_messages: [ + ...chatsRef.current[MOCK_CHAT.id]!.messages[0]!.response_messages, + newFileMessage + ], + isCompletedStream: false + } + ] + }; + + chatsRef.current[MOCK_CHAT.id] = myChat; + + startTransition(() => { + chatsRef.current = { ...chatsRef.current }; + }); + }); + return { unsubscribeFromChat, subscribeToChat diff --git a/web/src/routes/busterRoutes/busterAppRoutes.ts b/web/src/routes/busterRoutes/busterAppRoutes.ts index 9ae606cfd..e227623d9 100644 --- a/web/src/routes/busterRoutes/busterAppRoutes.ts +++ b/web/src/routes/busterRoutes/busterAppRoutes.ts @@ -24,7 +24,7 @@ export enum BusterAppRoutes { //NEW CHAT APP_CHAT_ID = '/app/chat/:chatId', - APP_CHAT_ID_REASONING_ID = '/app/chat/:chatId/reasoning/:reasoningId', + APP_CHAT_ID_REASONING_ID = '/app/chat/:chatId/reasoning/:messageId', APP_CHAT_ID_METRIC_ID = '/app/chat/:chatId/metric/:metricId', APP_CHAT_ID_COLLECTION_ID = '/app/chat/:chatId/collection/:collectionId', APP_CHAT_ID_DASHBOARD_ID = '/app/chat/:chatId/dashboard/:dashboardId',