From dc152628f090324f5642ddfa4bce2256b2443c0c Mon Sep 17 00:00:00 2001 From: Nate Kelley Date: Tue, 28 Jan 2025 17:12:23 -0700 Subject: [PATCH] hidden items --- .../chats/chatMessageInterfaces.ts | 1 + .../ChatResponseMessageHidden.tsx | 135 ++++++++++++++++++ .../ChatResponseMessageSelector.tsx | 49 +++++++ .../ChatResponseMessage_File.tsx | 8 +- .../ChatResponseMessage_Text.tsx | 8 +- .../ChatResponseMessage_Thought.tsx | 2 +- .../ChatResponseMessages.tsx | 64 +++++---- .../ChatResponseMessages/config.ts | 0 web/src/context/Chats/MOCK_CHAT.ts | 2 +- 9 files changed, 237 insertions(+), 32 deletions(-) create mode 100644 web/src/app/app/_layouts/ChatLayout/ChatContainer/ChatContent/ChatResponseMessages/ChatResponseMessageHidden.tsx create mode 100644 web/src/app/app/_layouts/ChatLayout/ChatContainer/ChatContent/ChatResponseMessages/ChatResponseMessageSelector.tsx create mode 100644 web/src/app/app/_layouts/ChatLayout/ChatContainer/ChatContent/ChatResponseMessages/config.ts diff --git a/web/src/api/buster_socket/chats/chatMessageInterfaces.ts b/web/src/api/buster_socket/chats/chatMessageInterfaces.ts index 8fb89dae3..03b2eaaf3 100644 --- a/web/src/api/buster_socket/chats/chatMessageInterfaces.ts +++ b/web/src/api/buster_socket/chats/chatMessageInterfaces.ts @@ -24,6 +24,7 @@ export type BusterChatMessage_text = { type: 'text'; message: string; message_chunk: string; + hidden?: boolean; }; export type BusterChatMessage_thoughtPill = { diff --git a/web/src/app/app/_layouts/ChatLayout/ChatContainer/ChatContent/ChatResponseMessages/ChatResponseMessageHidden.tsx b/web/src/app/app/_layouts/ChatLayout/ChatContainer/ChatContent/ChatResponseMessages/ChatResponseMessageHidden.tsx new file mode 100644 index 000000000..39ca72c0b --- /dev/null +++ b/web/src/app/app/_layouts/ChatLayout/ChatContainer/ChatContent/ChatResponseMessages/ChatResponseMessageHidden.tsx @@ -0,0 +1,135 @@ +import React, { useState, useRef, useMemo } from 'react'; +import type { BusterChatMessageResponse } from '@/api/buster_socket/chats'; +import { createStyles } from 'antd-style'; +import { ChatResponseMessageSelector } from './ChatResponseMessageSelector'; +import { AnimatePresence, motion } from 'framer-motion'; +import { Text } from '@/components/text'; +import { AppMaterialIcons } from '@/components'; +import pluralize from 'pluralize'; +import { useMemoizedFn } from 'ahooks'; + +const animationConfig = { + initial: { opacity: 0, height: 0 }, + animate: { + height: 'auto', + opacity: 1, + transition: { + height: { duration: 0.25, ease: 'easeOut' }, + opacity: { duration: 0.175, ease: 'easeOut' } + } + }, + exit: { + opacity: 0, + height: 0, + transition: { + height: { duration: 0.25, ease: 'easeIn' }, + opacity: { duration: 0.175, ease: 'easeIn' } + } + } +}; + +export const ChatResponseMessageHidden: React.FC<{ + hiddenItems: BusterChatMessageResponse[]; + isCompletedStream: boolean; + selectedFileId: string | undefined; +}> = React.memo(({ hiddenItems, isCompletedStream, selectedFileId }) => { + const { styles, cx } = useStyles(); + const [isHidden, setIsHidden] = useState(true); + + const onToggleHidden = useMemoizedFn(() => { + setIsHidden(!isHidden); + }); + + return ( +
+ + + {!isHidden && ( + +
+ {hiddenItems.map((item) => ( +
+ +
+ ))} +
+
+ )} +
+
+ ); +}); + +const hideAnimationConfig = { + initial: { opacity: 1, scaleY: 0 }, + animate: { opacity: 1, scaleY: 1 }, + exit: { opacity: 1, scaleY: 0 }, + transition: { duration: 0.125, ease: 'easeOut' } +}; + +const HideButton: React.FC<{ onClick: () => void; numerOfItems: number; isHidden: boolean }> = ({ + onClick, + numerOfItems, + isHidden +}) => { + const { styles, cx } = useStyles(); + const text = useMemo( + () => + isHidden + ? `View ${numerOfItems} more ${pluralize('action', numerOfItems)}` + : `Hide ${numerOfItems} ${pluralize('action', numerOfItems)}`, + [isHidden, numerOfItems] + ); + const icon = isHidden ? 'unfold_more' : 'unfold_less'; + + return ( +
+
+ + + + + + {text} +
+
+ ); +}; + +ChatResponseMessageHidden.displayName = 'ChatResponseMessageHidden'; + +const useStyles = createStyles(({ token, css }) => ({ + hiddenCard: css` + margin-bottom: 4px; + `, + motionContainer: css` + overflow: hidden; + `, + unfoldIcon: css` + color: ${token.colorIcon}; + `, + hideButton: css` + border-radius: ${token.borderRadius}px; + background: transparent; + padding: 1px 4px; + + &:hover { + background: ${token.controlItemBgActive}; + } + ` +})); 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 new file mode 100644 index 000000000..bc6951129 --- /dev/null +++ b/web/src/app/app/_layouts/ChatLayout/ChatContainer/ChatContent/ChatResponseMessages/ChatResponseMessageSelector.tsx @@ -0,0 +1,49 @@ +import React from 'react'; +import { ChatResponseMessage_File } from './ChatResponseMessage_File'; +import { ChatResponseMessage_Text } from './ChatResponseMessage_Text'; +import { ChatResponseMessage_Thought } from './ChatResponseMessage_Thought'; +import type { BusterChatMessageResponse } from '@/api/buster_socket/chats'; +import { ChatResponseMessageHidden } from './ChatResponseMessageHidden'; + +export interface ChatResponseMessageProps { + responseMessage: BusterChatMessageResponse; + isCompletedStream: boolean; + isLastMessageItem: boolean; + isSelectedFile: boolean; +} + +const ChatResponseMessageRecord: Record< + BusterChatMessageResponse['type'], + React.FC +> = { + text: ChatResponseMessage_Text, + file: ChatResponseMessage_File, + thought: ChatResponseMessage_Thought +}; + +export const ChatResponseMessageSelector: React.FC<{ + responseMessage: BusterChatMessageResponse | BusterChatMessageResponse[]; + isCompletedStream: boolean; + isLastMessageItem: boolean; + selectedFileId: string | undefined; +}> = ({ responseMessage, isCompletedStream, isLastMessageItem, selectedFileId }) => { + if (Array.isArray(responseMessage)) { + return ( + + ); + } + + const ChatResponseMessage = ChatResponseMessageRecord[responseMessage.type]; + return ( + + ); +}; 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 index d10085b82..244e2cc6d 100644 --- 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 @@ -1,5 +1,5 @@ import React, { useMemo } from 'react'; -import { ChatResponseMessageProps } from '../ChatResponseMessages'; +import { ChatResponseMessageProps } from '../ChatResponseMessageSelector'; import { createStyles } from 'antd-style'; import type { BusterChatMessage_file, @@ -135,6 +135,12 @@ const useStyles = createStyles(({ token, css }) => ({ } } + .hidden-card + & { + .vertical-divider.top-line { + display: none; + } + } + &:last-child { .vertical-divider.bottom-line { display: none; 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 0816920b6..cdfde809e 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 @@ -2,7 +2,7 @@ import { BusterChatMessage_text } from '@/api/buster_socket/chats'; import React, { useEffect, useState } from 'react'; import { AnimatePresence, motion } from 'framer-motion'; import { animationConfig } from './animationConfig'; -import { ChatResponseMessageProps } from './ChatResponseMessages'; +import { ChatResponseMessageProps } from './ChatResponseMessageSelector'; import { createStyles } from 'antd-style'; export const ChatResponseMessage_Text: React.FC = React.memo( @@ -47,8 +47,8 @@ ChatResponseMessage_Text.displayName = 'ChatResponseMessage_Text'; const useStyles = createStyles(({ token, css }) => ({ textCard: css` - &.text-card:has(+ .thought-card) { - margin-bottom: 14px; - } + // &.text-card:has(+ .thought-card) { + margin-bottom: 14px; + // } ` })); 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 22089a99c..7994fcbbb 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 @@ -1,6 +1,6 @@ import { BusterChatMessage_thought } from '@/api/buster_socket/chats'; import React from 'react'; -import { ChatResponseMessageProps } from '../ChatResponseMessages'; +import { ChatResponseMessageProps } from '../ChatResponseMessageSelector'; import { AnimatePresence, motion } from 'framer-motion'; import { animationConfig } from '../animationConfig'; import { Text } from '@/components/text'; 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 345005837..ea9a0aa55 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,25 +1,13 @@ -import React from 'react'; +import React, { useCallback, useMemo } from 'react'; import type { BusterChatMessageResponse } from '@/api/buster_socket/chats'; import { MessageContainer } from '../MessageContainer'; import { ChatResponseMessage_File } from './ChatResponseMessage_File'; import { ChatResponseMessage_Text } from './ChatResponseMessage_Text'; import { ChatResponseMessage_Thought } from './ChatResponseMessage_Thought'; - -export interface ChatResponseMessageProps { - responseMessage: BusterChatMessageResponse; - isCompletedStream: boolean; - isLastMessageItem: boolean; - isSelectedFile: boolean; -} - -const ChatResponseMessageRecord: Record< - BusterChatMessageResponse['type'], - React.FC -> = { - text: ChatResponseMessage_Text, - file: ChatResponseMessage_File, - thought: ChatResponseMessage_Thought -}; +import { AnimatePresence } from 'framer-motion'; +import { ChatResponseMessageHidden } from './ChatResponseMessageHidden'; +import { useMemoizedFn } from 'ahooks'; +import { ChatResponseMessageSelector } from './ChatResponseMessageSelector'; interface ChatResponseMessagesProps { responseMessages: BusterChatMessageResponse[]; @@ -27,21 +15,49 @@ interface ChatResponseMessagesProps { isCompletedStream: boolean; } +type ResponseMessageWithHiddenClusters = BusterChatMessageResponse | BusterChatMessageResponse[]; + export const ChatResponseMessages: React.FC = React.memo( - ({ responseMessages, isCompletedStream, selectedFileId }) => { - const lastMessageIndex = responseMessages.length - 1; + ({ responseMessages: responseMessagesProp, isCompletedStream, selectedFileId }) => { + const lastMessageIndex = responseMessagesProp.length - 1; + + const responseMessages: ResponseMessageWithHiddenClusters[] = useMemo(() => { + return responseMessagesProp.reduce( + (acc, responseMessage, index) => { + const isHidden = responseMessage.hidden; + const isPreviousHidden = responseMessagesProp[index - 1]?.hidden; + if (isHidden && isPreviousHidden) { + const currentCluster = acc[acc.length - 1] as BusterChatMessageResponse[]; + currentCluster.push(responseMessage); + return acc; + } else if (isHidden) { + acc.push([responseMessage]); + return acc; + } + acc.push(responseMessage); + return acc; + }, + [] + ); + }, [responseMessagesProp]); + + const getKey = useMemoizedFn((responseMessage: ResponseMessageWithHiddenClusters) => { + if (Array.isArray(responseMessage)) { + return responseMessage.map((item) => item.id).join('-'); + } + return responseMessage.id; + }); return ( {responseMessages.map((responseMessage, index) => { - const ChatResponseMessage = ChatResponseMessageRecord[responseMessage.type]; return ( - ); })} @@ -49,5 +65,3 @@ export const ChatResponseMessages: React.FC = React.m ); } ); - -ChatResponseMessages.displayName = 'ChatResponseMessages'; diff --git a/web/src/app/app/_layouts/ChatLayout/ChatContainer/ChatContent/ChatResponseMessages/config.ts b/web/src/app/app/_layouts/ChatLayout/ChatContainer/ChatContent/ChatResponseMessages/config.ts new file mode 100644 index 000000000..e69de29bb diff --git a/web/src/context/Chats/MOCK_CHAT.ts b/web/src/context/Chats/MOCK_CHAT.ts index 098968bc9..864e70052 100644 --- a/web/src/context/Chats/MOCK_CHAT.ts +++ b/web/src/context/Chats/MOCK_CHAT.ts @@ -45,7 +45,7 @@ export const createMockResponseMessageThought = (): BusterChatMessage_thought => thought_title: `Found ${faker.number.int(100)} terms`, thought_secondary_title: faker.lorem.word(), thought_pills: fourRandomPills, - hidden: false, + hidden: true, status: undefined }; };