mirror of https://github.com/buster-so/buster.git
hidden items
This commit is contained in:
parent
44df60c695
commit
dc152628f0
|
@ -24,6 +24,7 @@ export type BusterChatMessage_text = {
|
|||
type: 'text';
|
||||
message: string;
|
||||
message_chunk: string;
|
||||
hidden?: boolean;
|
||||
};
|
||||
|
||||
export type BusterChatMessage_thoughtPill = {
|
||||
|
|
|
@ -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};
|
||||
}
|
||||
`
|
||||
}));
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
// }
|
||||
`
|
||||
}));
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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
|
||||
};
|
||||
};
|
||||
|
|
Loading…
Reference in New Issue