hidden items

This commit is contained in:
Nate Kelley 2025-01-28 17:12:23 -07:00
parent 44df60c695
commit dc152628f0
No known key found for this signature in database
GPG Key ID: FD90372AB8D98B4F
9 changed files with 237 additions and 32 deletions

View File

@ -24,6 +24,7 @@ export type BusterChatMessage_text = {
type: 'text';
message: string;
message_chunk: string;
hidden?: boolean;
};
export type BusterChatMessage_thoughtPill = {

View File

@ -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 (
<div className={cx('hidden-card', styles.hiddenCard)}>
<HideButton onClick={onToggleHidden} numerOfItems={hiddenItems.length} isHidden={isHidden} />
<AnimatePresence initial={false}>
{!isHidden && (
<motion.div
className={styles.motionContainer}
initial={animationConfig.initial}
animate={animationConfig.animate}
exit={animationConfig.exit}>
<div>
{hiddenItems.map((item) => (
<div className="" key={item.id}>
<ChatResponseMessageSelector
key={item.id}
responseMessage={item}
isCompletedStream={isCompletedStream}
isLastMessageItem={false}
selectedFileId={selectedFileId}
/>
</div>
))}
</div>
</motion.div>
)}
</AnimatePresence>
</div>
);
});
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 (
<div className="mb-1 flex flex-col">
<div
className={cx('ml-1 flex w-fit cursor-pointer items-center space-x-1', styles.hideButton)}
onClick={onClick}>
<AnimatePresence mode="wait" initial={false}>
<motion.div
key={isHidden ? 'hidden' : 'visible'}
className={cx('flex w-4 min-w-4 items-center justify-center', styles.unfoldIcon)}
{...hideAnimationConfig}>
<AppMaterialIcons size={12} icon={icon} />
</motion.div>
</AnimatePresence>
<Text className="pointer-events-none">{text}</Text>
</div>
</div>
);
};
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};
}
`
}));

View File

@ -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<ChatResponseMessageProps>
> = {
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 (
<ChatResponseMessageHidden
hiddenItems={responseMessage}
isCompletedStream={isCompletedStream}
selectedFileId={selectedFileId}
/>
);
}
const ChatResponseMessage = ChatResponseMessageRecord[responseMessage.type];
return (
<ChatResponseMessage
responseMessage={responseMessage}
isCompletedStream={isCompletedStream}
isLastMessageItem={isLastMessageItem}
isSelectedFile={responseMessage.id === selectedFileId}
/>
);
};

View File

@ -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;

View File

@ -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<ChatResponseMessageProps> = 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;
// }
`
}));

View File

@ -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';

View File

@ -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<ChatResponseMessageProps>
> = {
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<ChatResponseMessagesProps> = 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<ResponseMessageWithHiddenClusters[]>(
(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 (
<MessageContainer className="flex w-full flex-col overflow-hidden">
{responseMessages.map((responseMessage, index) => {
const ChatResponseMessage = ChatResponseMessageRecord[responseMessage.type];
return (
<ChatResponseMessage
key={responseMessage.id}
<ChatResponseMessageSelector
key={getKey(responseMessage)}
responseMessage={responseMessage}
isCompletedStream={isCompletedStream}
isLastMessageItem={index === lastMessageIndex}
isSelectedFile={responseMessage.id === selectedFileId}
selectedFileId={selectedFileId}
/>
);
})}
@ -49,5 +65,3 @@ export const ChatResponseMessages: React.FC<ChatResponseMessagesProps> = React.m
);
}
);
ChatResponseMessages.displayName = 'ChatResponseMessages';

View File

@ -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
};
};