diff --git a/web/src/api/buster_socket/chats/chatMessageInterfaces.ts b/web/src/api/buster_socket/chats/chatMessageInterfaces.ts index 5674ed3b0..45e31b677 100644 --- a/web/src/api/buster_socket/chats/chatMessageInterfaces.ts +++ b/web/src/api/buster_socket/chats/chatMessageInterfaces.ts @@ -39,19 +39,20 @@ export type BusterChatMessage_thought = { thought_secondary_title: string; thought_pills?: BusterChatMessage_thoughtPill[]; hidden?: boolean; //if left undefined, will automatically be set to false if stream has ended - in_progress?: boolean; //if left undefined, will automatically be set to true if the chat stream is in progress AND there is no message after it + status?: 'loading' | 'completed' | 'failed'; //if left undefined, will automatically be set to 'loading' if the chat stream is in progress AND there is no message after it }; export type BusterChatMessage_fileMetadata = { status: 'loading' | 'completed' | 'failed'; message: string; - timestamp?: number; + timestamp?: string; }; export type BusterChatMessage_file = { id: string; type: 'file'; file_type: FileType; + file_name: string; version_number: number; version_id: string; metadata?: BusterChatMessage_fileMetadata[]; diff --git a/web/src/app/app/_layouts/ChatLayout/ChatContainer/ChatContent/ChatInput/ChatInput.tsx b/web/src/app/app/_layouts/ChatLayout/ChatContainer/ChatContent/ChatInput/ChatInput.tsx index cd35bbc3f..4dcd11637 100644 --- a/web/src/app/app/_layouts/ChatLayout/ChatContainer/ChatContent/ChatInput/ChatInput.tsx +++ b/web/src/app/app/_layouts/ChatLayout/ChatContainer/ChatContent/ChatInput/ChatInput.tsx @@ -1,24 +1,64 @@ -import React from 'react'; +import React, { useMemo, useState } from 'react'; import { Input, Button } from 'antd'; import { Text } from '@/components/text'; import { createStyles } from 'antd-style'; +import { useMemoizedFn } from 'ahooks'; +import { AppMaterialIcons } from '@/components'; +import { inputHasText } from '@/utils'; -export const ChatInput: React.FC = () => { +const autoSize = { minRows: 4, maxRows: 4 }; + +export const ChatInput: React.FC = React.memo(() => { const { styles, cx } = useStyles(); + const [inputValue, setInputValue] = useState(''); + + const disableSendButton = useMemo(() => { + return !inputHasText(inputValue); + }, [inputValue]); + + const handleInputChange = useMemoizedFn((e: React.ChangeEvent) => { + setInputValue(e.target.value); + }); + + const onSubmit = useMemoizedFn(async () => { + if (disableSendButton) return; + console.log('submit'); + }); + return (
- +
+ + +
+
+
Our AI may make mistakes. Check important info.
); -}; +}); + +ChatInput.displayName = 'ChatInput'; const useStyles = createStyles(({ token, css }) => ({ inputContainer: css` diff --git a/web/src/app/app/_layouts/ChatLayout/ChatContainer/ChatContent/ChatResponseMessages/ChatResponseMessage_File.tsx b/web/src/app/app/_layouts/ChatLayout/ChatContainer/ChatContent/ChatResponseMessages/ChatResponseMessage_File.tsx deleted file mode 100644 index b72a07758..000000000 --- a/web/src/app/app/_layouts/ChatLayout/ChatContainer/ChatContent/ChatResponseMessages/ChatResponseMessage_File.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import React from 'react'; -import { ChatResponseMessageProps } from './ChatResponseMessages'; - -export const ChatResponseMessage_File: React.FC = React.memo( - ({ responseMessage }) => { - return
ChatResponseMessage_File
; - } -); - -ChatResponseMessage_File.displayName = 'ChatResponseMessage_File'; diff --git a/web/src/app/app/_layouts/ChatLayout/ChatContainer/ChatContent/ChatResponseMessages/ChatResponseMessage_File/ChatResponseMessage_File.tsx b/web/src/app/app/_layouts/ChatLayout/ChatContainer/ChatContent/ChatResponseMessages/ChatResponseMessage_File/ChatResponseMessage_File.tsx new file mode 100644 index 000000000..b86593603 --- /dev/null +++ b/web/src/app/app/_layouts/ChatLayout/ChatContainer/ChatContent/ChatResponseMessages/ChatResponseMessage_File/ChatResponseMessage_File.tsx @@ -0,0 +1,173 @@ +import React, { useMemo } from 'react'; +import { ChatResponseMessageProps } from '../ChatResponseMessages'; +import { createStyles } from 'antd-style'; +import type { + BusterChatMessage_file, + BusterChatMessage_fileMetadata +} from '@/api/buster_socket/chats'; +import { Text } from '@/components/text'; +import { motion, AnimatePresence } from 'framer-motion'; +import { animationConfig } from '../animationConfig'; +import { useMemoizedFn } from 'ahooks'; +import { StatusIndicator } from '../StatusIndicator'; +import { formatDate } from '@/utils'; + +export const ChatResponseMessage_File: React.FC = React.memo( + ({ responseMessage: responseMessageProp, isCompletedStream }) => { + const { cx, styles } = useStyles(); + const responseMessage = responseMessageProp as BusterChatMessage_file; + const { file_name, file_type, version_id, version_number, id, metadata = [] } = responseMessage; + + const onClickCard = useMemoizedFn(() => { + console.log('clicked'); + }); + + return ( + + + +
+ + +
+ +
+
+ ); + } +); + +ChatResponseMessage_File.displayName = 'ChatResponseMessage_File'; + +const ChatResponseMessageHeader: React.FC<{ file_name: string; version_number: number }> = ({ + file_name, + version_number +}) => { + const { cx, styles } = useStyles(); + return ( +
+ {file_name} +
+ + v{version_number} + +
+
+ ); +}; + +const ChatResponseMessageBody: React.FC<{ + metadata: BusterChatMessage_fileMetadata[]; +}> = ({ metadata }) => { + return ( +
+ {metadata.map((metadata, index) => ( + + ))} +
+ ); +}; + +const MetadataItem: React.FC<{ metadata: BusterChatMessage_fileMetadata }> = ({ metadata }) => { + const { status, message, timestamp } = metadata; + + const timestampFormatted = useMemo(() => { + if (!timestamp) return ''; + return formatDate({ + date: timestamp, + format: 'll' + }); + }, [timestamp]); + + return ( +
+
+ +
+ + + {message} + + + {timestamp && ( + + {timestampFormatted} + + )} +
+ ); +}; + +const VerticalDivider: React.FC<{ className?: string }> = ({ className }) => { + const { cx, styles } = useStyles(); + return
; +}; + +const useStyles = createStyles(({ token, css }) => ({ + fileCard: css` + & + .file-card { + .vertical-divider.top-line { + display: none; + } + } + + &.file-card { + .thought-card + & { + .vertical-divider.top-line { + display: none; + } + } + } + + &:last-child { + .vertical-divider.bottom-line { + display: none; + } + } + `, + fileContainer: css` + border-radius: ${token.borderRadius}px; + border: 0.5px solid ${token.colorBorder}; + + &:hover { + border-color: ${token.colorTextTertiary}; + box-shadow: ${token.boxShadowTertiary}; + } + + &.selected { + border-color: black; + box-shadow: ${token.boxShadowTertiary}; + } + `, + fileHeader: css` + background: ${token.controlItemBgActive}; + border-bottom: 0.5px solid ${token.colorBorder}; + height: 32px; + `, + fileVersion: css` + border-radius: ${token.borderRadius}px; + padding: 4px; + background: ${token.colorFillTertiary}; + `, + verticalDivider: css` + height: 9px; + width: 0.5px; + margin-left: 8px; + background: ${token.colorTextTertiary}; + ` +})); diff --git a/web/src/app/app/_layouts/ChatLayout/ChatContainer/ChatContent/ChatResponseMessages/ChatResponseMessage_File/index.ts b/web/src/app/app/_layouts/ChatLayout/ChatContainer/ChatContent/ChatResponseMessages/ChatResponseMessage_File/index.ts new file mode 100644 index 000000000..991a4496a --- /dev/null +++ b/web/src/app/app/_layouts/ChatLayout/ChatContainer/ChatContent/ChatResponseMessages/ChatResponseMessage_File/index.ts @@ -0,0 +1 @@ +export * from './ChatResponseMessage_File'; diff --git a/web/src/app/app/_layouts/ChatLayout/ChatContainer/ChatContent/ChatResponseMessages/ChatResponseMessage_Text.tsx b/web/src/app/app/_layouts/ChatLayout/ChatContainer/ChatContent/ChatResponseMessages/ChatResponseMessage_Text.tsx index 244b9b04b..9a853bc7f 100644 --- a/web/src/app/app/_layouts/ChatLayout/ChatContainer/ChatContent/ChatResponseMessages/ChatResponseMessage_Text.tsx +++ b/web/src/app/app/_layouts/ChatLayout/ChatContainer/ChatContent/ChatResponseMessages/ChatResponseMessage_Text.tsx @@ -30,7 +30,7 @@ export const ChatResponseMessage_Text: React.FC = Reac }, [responseMessage?.message_chunk, responseMessage?.message]); return ( -
+
{textChunks.map((chunk, index) => ( {chunk} diff --git a/web/src/app/app/_layouts/ChatLayout/ChatContainer/ChatContent/ChatResponseMessages/ChatResponseMessage_Thought/ChatResponseMessage_Thought.tsx b/web/src/app/app/_layouts/ChatLayout/ChatContainer/ChatContent/ChatResponseMessages/ChatResponseMessage_Thought/ChatResponseMessage_Thought.tsx index 7b216230d..30ab87c18 100644 --- a/web/src/app/app/_layouts/ChatLayout/ChatContainer/ChatContent/ChatResponseMessages/ChatResponseMessage_Thought/ChatResponseMessage_Thought.tsx +++ b/web/src/app/app/_layouts/ChatLayout/ChatContainer/ChatContent/ChatResponseMessages/ChatResponseMessage_Thought/ChatResponseMessage_Thought.tsx @@ -6,24 +6,28 @@ import { animationConfig } from '../animationConfig'; import { Text } from '@/components/text'; import { createStyles } from 'antd-style'; import { PillContainer } from './ChatResponseMessage_ThoughtPills'; -import { StatusIndicator } from './StatusIndicator'; +import { StatusIndicator } from '../StatusIndicator'; import { VerticalBar } from './VerticalBar'; export const ChatResponseMessage_Thought: React.FC = React.memo( ({ responseMessage: responseMessageProp, isCompletedStream, isLastMessageItem }) => { const responseMessage = responseMessageProp as BusterChatMessage_thought; - const { thought_title, thought_secondary_title, thought_pills, in_progress } = responseMessage; + const { thought_title, thought_secondary_title, thought_pills, status } = responseMessage; const { cx } = useStyles(); const hasPills = thought_pills && thought_pills.length > 0; - const showLoadingIndicator = in_progress ?? (isLastMessageItem && !isCompletedStream); + const showLoadingIndicator = + (status ?? (isLastMessageItem && !isCompletedStream)) ? 'loading' : 'completed'; + const inProgress = showLoadingIndicator === 'loading'; return ( - +
- - + +
diff --git a/web/src/app/app/_layouts/ChatLayout/ChatContainer/ChatContent/ChatResponseMessages/ChatResponseMessage_Thought/ChatResponseMessage_ThoughtPills.tsx b/web/src/app/app/_layouts/ChatLayout/ChatContainer/ChatContent/ChatResponseMessages/ChatResponseMessage_Thought/ChatResponseMessage_ThoughtPills.tsx index bef7f4246..ac5622ba1 100644 --- a/web/src/app/app/_layouts/ChatLayout/ChatContainer/ChatContent/ChatResponseMessages/ChatResponseMessage_Thought/ChatResponseMessage_ThoughtPills.tsx +++ b/web/src/app/app/_layouts/ChatLayout/ChatContainer/ChatContent/ChatResponseMessages/ChatResponseMessage_Thought/ChatResponseMessage_ThoughtPills.tsx @@ -1,9 +1,9 @@ -import { +import type { BusterChatMessage_thought, BusterChatMessage_thoughtPill } from '@/api/buster_socket/chats'; import { createStyles } from 'antd-style'; -import React, { useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'; +import React, { useLayoutEffect, useMemo, useRef, useState } from 'react'; import { AnimatePresence, motion } from 'framer-motion'; import { calculateTextWidth } from '@/utils'; import { useDebounce, useMemoizedFn, useSize } from 'ahooks'; diff --git a/web/src/app/app/_layouts/ChatLayout/ChatContainer/ChatContent/ChatResponseMessages/ChatResponseMessage_Thought/StatusIndicator.tsx b/web/src/app/app/_layouts/ChatLayout/ChatContainer/ChatContent/ChatResponseMessages/ChatResponseMessage_Thought/StatusIndicator.tsx deleted file mode 100644 index dcd8def22..000000000 --- a/web/src/app/app/_layouts/ChatLayout/ChatContainer/ChatContent/ChatResponseMessages/ChatResponseMessage_Thought/StatusIndicator.tsx +++ /dev/null @@ -1,68 +0,0 @@ -import React from 'react'; -import { AnimatePresence, motion } from 'framer-motion'; -import { createStyles } from 'antd-style'; -import { CircleSpinnerLoader } from '@/components/loaders/CircleSpinnerLoader'; -import { AppMaterialIcons } from '@/components/icons'; - -const animationConfig = { - initial: { opacity: 0 }, - animate: { opacity: 1 }, - exit: { opacity: 0 }, - transition: { duration: 0.25 } -}; - -export const StatusIndicator: React.FC<{ inProgress?: boolean }> = React.memo(({ inProgress }) => { - const { styles, cx } = useStyles(); - return ( -
- - - {inProgress ? ( - - ) : ( - - )} - - -
- ); -}); - -StatusIndicator.displayName = 'StatusIndicator'; - -const useStyles = createStyles(({ token, css }) => ({ - indicatorContainer: css` - width: 10px; - height: 10px; - background-color: ${token.colorTextPlaceholder}; - border-radius: 100%; - - &.in-progress { - background-color: transparent; - } - `, - indicator: css` - color: white; - padding: 1px; - border-radius: 100%; - background-color: ${token.colorTextPlaceholder}; - box-shadow: 0 0 0 0.7px white inset; - - &.in-progress { - background-color: transparent; - box-shadow: none; - } - ` -})); 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 d8346caa3..5f59d5b7c 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 @@ -30,7 +30,7 @@ export const ChatResponseMessages: React.FC = React.m const lastMessageIndex = responseMessages.length - 1; return ( - + {responseMessages.map((responseMessage, index) => { const ChatResponseMessage = ChatResponseMessageRecord[responseMessage.type]; return ( diff --git a/web/src/app/app/_layouts/ChatLayout/ChatContainer/ChatContent/ChatResponseMessages/StatusIndicator.tsx b/web/src/app/app/_layouts/ChatLayout/ChatContainer/ChatContent/ChatResponseMessages/StatusIndicator.tsx new file mode 100644 index 000000000..f34bdd63b --- /dev/null +++ b/web/src/app/app/_layouts/ChatLayout/ChatContainer/ChatContent/ChatResponseMessages/StatusIndicator.tsx @@ -0,0 +1,71 @@ +import React from 'react'; +import { AnimatePresence, motion } from 'framer-motion'; +import { createStyles } from 'antd-style'; +import { CircleSpinnerLoader } from '@/components/loaders/CircleSpinnerLoader'; +import { AppMaterialIcons } from '@/components/icons'; + +const animationConfig = { + initial: { opacity: 0 }, + animate: { opacity: 1 }, + exit: { opacity: 0 }, + transition: { duration: 0.25 } +}; + +export const StatusIndicator: React.FC<{ status?: 'completed' | 'loading' | 'failed' }> = + React.memo(({ status }) => { + const { styles, cx } = useStyles(); + const inProgress = status === 'loading'; + + return ( +
+ + + {inProgress ? ( + + ) : ( + + )} + + +
+ ); + }); + +StatusIndicator.displayName = 'StatusIndicator'; + +const useStyles = createStyles(({ token, css }) => ({ + indicatorContainer: css` + width: 10px; + height: 10px; + background-color: ${token.colorTextPlaceholder}; + border-radius: 100%; + + &.in-progress { + background-color: transparent; + } + `, + indicator: css` + color: white; + padding: 1px; + border-radius: 100%; + background-color: ${token.colorTextPlaceholder}; + box-shadow: 0 0 0 0.7px white inset; + + &.in-progress { + background-color: transparent; + box-shadow: none; + } + ` +})); diff --git a/web/src/app/app/_layouts/ChatLayout/ChatContainer/ChatContent/MessageContainer.tsx b/web/src/app/app/_layouts/ChatLayout/ChatContainer/ChatContent/MessageContainer.tsx index 2a2b32a44..249092ff4 100644 --- a/web/src/app/app/_layouts/ChatLayout/ChatContainer/ChatContent/MessageContainer.tsx +++ b/web/src/app/app/_layouts/ChatLayout/ChatContainer/ChatContent/MessageContainer.tsx @@ -11,7 +11,7 @@ export const MessageContainer: React.FC<{ }> = React.memo(({ children, senderName, senderId, senderAvatar, className = '' }) => { const { cx } = useStyles(); return ( -
+
{senderName ? ( ) : ( diff --git a/web/src/context/Chats/MOCK_CHAT.ts b/web/src/context/Chats/MOCK_CHAT.ts index beac77efd..ddf274dfb 100644 --- a/web/src/context/Chats/MOCK_CHAT.ts +++ b/web/src/context/Chats/MOCK_CHAT.ts @@ -6,7 +6,8 @@ import { type BusterChatMessageRequest, type BusterChatMessageResponse, FileType, - BusterChatMessage_thoughtPill + BusterChatMessage_thoughtPill, + BusterChatMessage_fileMetadata } from '@/api/buster_socket/chats'; import { faker } from '@faker-js/faker'; @@ -45,17 +46,31 @@ export const createMockResponseMessageThought = (): BusterChatMessage_thought => thought_secondary_title: faker.lorem.word(), thought_pills: fourRandomPills, hidden: false, - in_progress: undefined + status: undefined }; }; const createMockResponseMessageFile = (): BusterChatMessage_file => { + const randomMetadataCount = faker.number.int(3); + const randomMetadata: BusterChatMessage_fileMetadata[] = Array.from( + { length: randomMetadataCount }, + () => { + return { + status: 'completed', + message: faker.lorem.sentence(), + timestamp: faker.date.recent().toISOString() + }; + } + ); + return { id: faker.string.uuid(), type: 'file', file_type: 'metric', version_number: 1, - version_id: faker.string.uuid() + version_id: faker.string.uuid(), + file_name: faker.company.name(), + metadata: randomMetadata }; }; @@ -72,22 +87,8 @@ export const MOCK_CHAT: BusterChat = { createMockResponseMessageText(), createMockResponseMessageThought(), createMockResponseMessageThought(), - createMockResponseMessageThought(), - createMockResponseMessageThought(), - createMockResponseMessageThought(), - createMockResponseMessageThought(), - createMockResponseMessageThought(), - createMockResponseMessageThought(), - createMockResponseMessageThought(), - createMockResponseMessageThought(), - createMockResponseMessageThought(), - createMockResponseMessageThought(), - createMockResponseMessageThought(), - createMockResponseMessageThought(), - createMockResponseMessageThought(), - createMockResponseMessageThought() - // createMockResponseMessageFile(), - // createMockResponseMessageFile() + createMockResponseMessageFile(), + createMockResponseMessageFile() ] } ],