thought transitions complete

This commit is contained in:
Nate Kelley 2025-01-28 14:07:30 -07:00
parent 2d8d2baa6f
commit f41c4da9ff
No known key found for this signature in database
GPG Key ID: FD90372AB8D98B4F
10 changed files with 196 additions and 186 deletions

View File

@ -2,9 +2,8 @@ import React from 'react';
import { useChatContextSelector } from '../../ChatContext';
import { ChatMessageBlock } from './ChatMessageBlock';
export const ChatContent: React.FC<{ chatContentRef: React.RefObject<HTMLDivElement> }> = ({
chatContentRef
}) => {
export const ChatContent: React.FC<{ chatContentRef: React.RefObject<HTMLDivElement> }> =
React.memo(({ chatContentRef }) => {
const chatMessages = useChatContextSelector((state) => state.chatMessages);
return (
@ -14,4 +13,6 @@ export const ChatContent: React.FC<{ chatContentRef: React.RefObject<HTMLDivElem
</div>
</div>
);
};
});
ChatContent.displayName = 'ChatContent';

View File

@ -1,122 +0,0 @@
import {
BusterChatMessage_thought,
BusterChatMessage_thoughtPill
} from '@/api/buster_socket/chats';
import React, { useState } from 'react';
import { ChatResponseMessageProps } from './ChatResponseMessages';
import { AnimatePresence, motion } from 'framer-motion';
import { animationConfig } from './animationConfig';
import { CircleSpinnerLoader } from '@/components/loaders/CircleSpinnerLoader';
import { Text } from '@/components/text';
import { createStyles } from 'antd-style';
import { AppMaterialIcons } from '@/components';
import { PillContainer } from './ChatResponseMessage_ThoughtPills';
export const ChatResponseMessage_Thought: React.FC<ChatResponseMessageProps> = React.memo(
({ responseMessage: responseMessageProp, isCompletedStream }) => {
const responseMessage = responseMessageProp as BusterChatMessage_thought;
const { thought_title, thought_secondary_title, thought_pills, in_progress } = responseMessage;
const { styles, cx } = useStyles();
const hasPills = thought_pills && thought_pills.length > 0;
return (
<AnimatePresence initial={!isCompletedStream}>
<motion.div className={cx(styles.container, 'flex space-x-1.5')} {...animationConfig}>
<div className="flex w-4 min-w-4 flex-col items-center pt-0.5">
<StatusIndicator inProgress={in_progress} />
<VerticalBar inProgress={in_progress} hasPills={hasPills} />
</div>
<div className="flex w-full flex-col space-y-2">
<div className="flex w-full items-center space-x-1.5 overflow-hidden">
<Text size="sm" className="truncate">
{thought_title}
</Text>
<Text size="sm" type="tertiary">
{thought_secondary_title}
</Text>
</div>
<PillContainer pills={thought_pills} isCompletedStream={isCompletedStream} />
</div>
</motion.div>
</AnimatePresence>
);
}
);
ChatResponseMessage_Thought.displayName = 'ChatResponseMessage_Thought';
const StatusIndicator: React.FC<{ inProgress?: boolean }> = ({ inProgress }) => {
const { styles, cx } = useStyles();
return (
<div
className={cx(
styles.indicatorContainer,
inProgress && 'in-progress',
'flex items-center justify-center'
)}>
<div
className={cx(
styles.indicator,
inProgress && 'in-progress',
'flex items-center justify-center'
)}>
{inProgress ? (
<CircleSpinnerLoader size={8} />
) : (
<AppMaterialIcons className="" icon="check" size={6} />
)}
</div>
</div>
);
};
const VerticalBar: React.FC<{ inProgress?: boolean; hasPills?: boolean }> = ({
inProgress,
hasPills
}) => {
const { styles, cx } = useStyles();
return (
<div
className={cx(
'flex w-full flex-1 items-center justify-center overflow-hidden',
// 'opacity-0',
'transition-opacity duration-300',
hasPills && 'opacity-100'
)}>
<div className={cx(styles.verticalBar, 'mt-1 overflow-hidden')} />
</div>
);
};
const useStyles = createStyles(({ token, css }) => ({
container: css`
position: relative;
`,
verticalBar: css`
width: 0.5px;
height: 100%;
background-color: ${token.colorTextPlaceholder};
`,
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;
}
`
}));

View File

@ -0,0 +1,48 @@
import { BusterChatMessage_thought } from '@/api/buster_socket/chats';
import React from 'react';
import { ChatResponseMessageProps } from '../ChatResponseMessages';
import { AnimatePresence, motion } from 'framer-motion';
import { animationConfig } from '../animationConfig';
import { Text } from '@/components/text';
import { createStyles } from 'antd-style';
import { PillContainer } from './ChatResponseMessage_ThoughtPills';
import { StatusIndicator } from './StatusIndicator';
import { VerticalBar } from './VerticalBar';
export const ChatResponseMessage_Thought: React.FC<ChatResponseMessageProps> = React.memo(
({ responseMessage: responseMessageProp, isCompletedStream, isLastMessageItem }) => {
const responseMessage = responseMessageProp as BusterChatMessage_thought;
const { thought_title, thought_secondary_title, thought_pills, in_progress } = responseMessage;
const { styles, cx } = useStyles();
const hasPills = thought_pills && thought_pills.length > 0;
const showLoadingIndicator = in_progress ?? (isLastMessageItem && !isCompletedStream);
return (
<AnimatePresence initial={!isCompletedStream}>
<motion.div className={cx('relative flex space-x-1.5')} {...animationConfig}>
<div className="flex w-4 min-w-4 flex-col items-center pt-0.5">
<StatusIndicator inProgress={showLoadingIndicator} />
<VerticalBar inProgress={in_progress} hasPills={hasPills} />
</div>
<div className="flex w-full flex-col space-y-2">
<div className="flex w-full items-center space-x-1.5 overflow-hidden">
<Text size="sm" className="truncate">
{thought_title}
</Text>
<Text size="sm" type="tertiary">
{thought_secondary_title}
</Text>
</div>
<PillContainer pills={thought_pills} isCompletedStream={isCompletedStream} />
</div>
</motion.div>
</AnimatePresence>
);
}
);
ChatResponseMessage_Thought.displayName = 'ChatResponseMessage_Thought';
const useStyles = createStyles(({ token, css }) => ({}));

View File

@ -8,7 +8,7 @@ import { AnimatePresence, motion } from 'framer-motion';
import { calculateTextWidth } from '@/utils';
import { useDebounce, useMemoizedFn, useSize } from 'ahooks';
import { AppPopover } from '@/components';
import { useChatLayoutContextSelector } from '../../../ChatLayoutContext';
import { useChatLayoutContextSelector } from '../../../../ChatLayoutContext';
const duration = 0.25;

View File

@ -0,0 +1,68 @@
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 (
<div
className={cx(
styles.indicatorContainer,
inProgress && 'in-progress',
'flex items-center justify-center transition-all delay-100 duration-300'
)}>
<AnimatePresence mode="wait">
<motion.div
key={inProgress ? 'in-progress' : 'completed'}
{...animationConfig}
className={cx(
styles.indicator,
inProgress && 'in-progress',
'flex items-center justify-center transition-all duration-300'
)}>
{inProgress ? (
<CircleSpinnerLoader size={8} />
) : (
<AppMaterialIcons className="" icon="check" size={6} />
)}
</motion.div>
</AnimatePresence>
</div>
);
});
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;
}
`
}));

View File

@ -0,0 +1,31 @@
import { createStyles } from 'antd-style';
import React from 'react';
export const VerticalBar: React.FC<{ inProgress?: boolean; hasPills?: boolean }> = ({
inProgress,
hasPills
}) => {
const { styles, cx } = useStyles();
return (
<div
className={cx(
'flex w-full flex-1 items-center justify-center overflow-hidden',
// 'opacity-0',
'transition-opacity duration-300',
hasPills && 'opacity-100'
)}>
<div className={cx(styles.verticalBar, 'mt-1 overflow-hidden')} />
</div>
);
};
const useStyles = createStyles(({ token, css }) => ({
container: css`
position: relative;
`,
verticalBar: css`
width: 0.5px;
height: 100%;
background-color: ${token.colorTextPlaceholder};
`
}));

View File

@ -0,0 +1 @@
export * from './ChatResponseMessage_Thought';

View File

@ -1,15 +1,14 @@
import React, { useEffect, useRef, useState } from 'react';
import type { BusterChatMessage_text, BusterChatMessageResponse } from '@/api/buster_socket/chats';
import React 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';
import { useHotkeys } from 'react-hotkeys-hook';
import { faker } from '@faker-js/faker';
export interface ChatResponseMessageProps {
responseMessage: BusterChatMessageResponse;
isCompletedStream: boolean;
isLastMessageItem: boolean;
}
const ChatResponseMessageRecord: Record<
@ -28,55 +27,18 @@ interface ChatResponseMessagesProps {
export const ChatResponseMessages: React.FC<ChatResponseMessagesProps> = React.memo(
({ responseMessages, isCompletedStream }) => {
// const [testMessages, setMessages] = useState<BusterChatMessageResponse[]>(responseMessages);
// const replicaOfMessages = useRef<string>('');
// useEffect(() => {
// setMessages(responseMessages);
// replicaOfMessages.current =
// (responseMessages as BusterChatMessage_text[])[0]?.message ||
// (responseMessages as BusterChatMessage_text[])[0]?.message_chunk ||
// '';
// }, [responseMessages]);
// const firstMessageId = testMessages[0]?.id;
// useHotkeys('x', () => {
// const threeRandomWords = ' ' + faker.lorem.words(6) + ' swag';
// setMessages((prevMessages) => {
// return prevMessages.map((message) => {
// if (message.id === firstMessageId) {
// replicaOfMessages.current = replicaOfMessages.current + threeRandomWords;
// return {
// ...message,
// message_chunk: threeRandomWords
// };
// }
// return message;
// });
// });
// });
// useHotkeys('z', () => {
// setMessages((prevMessages) => {
// return prevMessages.map((message) => {
// if (message.id === firstMessageId) {
// return { ...message, message: replicaOfMessages.current };
// }
// return message;
// });
// });
// });
const lastMessageIndex = responseMessages.length - 1;
return (
<MessageContainer className="flex w-full flex-col space-y-1">
{responseMessages.map((responseMessage) => {
{responseMessages.map((responseMessage, index) => {
const ChatResponseMessage = ChatResponseMessageRecord[responseMessage.type];
return (
<ChatResponseMessage
key={responseMessage.id}
responseMessage={responseMessage}
isCompletedStream={isCompletedStream}
isLastMessageItem={index === lastMessageIndex}
/>
);
})}

View File

@ -5,12 +5,14 @@ import {
useContextSelector
} from '@fluentui/react-context-selector';
import { useBusterWebSocket } from '../BusterWebSocket';
import type { BusterChatAsset, BusterChat } from '@/api/buster_socket/chats';
import type { BusterChatAsset, BusterChat, BusterChatMessage } from '@/api/buster_socket/chats';
import { useMemoizedFn, useUnmount } from 'ahooks';
import type { FileType } from '@/api/buster_socket/chats';
import { MOCK_CHAT } from './MOCK_CHAT';
import { createMockResponseMessageThought, MOCK_CHAT } from './MOCK_CHAT';
import { IBusterChat } from './interfaces';
import { chatUpgrader } from './helpers';
import { chatMessageUpgrader, chatUpgrader } from './helpers';
import { useHotkeys } from 'react-hotkeys-hook';
import { fi } from '@faker-js/faker';
export const useBusterChat = () => {
const busterSocket = useBusterWebSocket();
@ -93,6 +95,25 @@ export const useBusterChat = () => {
}
);
useHotkeys('z', () => {
const chatId = Object.keys(chatsRef.current)[0];
if (chatId) {
const chat = chatsRef.current[chatId];
const mockMessage = createMockResponseMessageThought();
const newChat = { ...chat };
const firstMessage = {
...newChat.messages[0],
isCompletedStream: false,
response_messages: [...newChat.messages[0].response_messages, mockMessage]
};
newChat.messages = [firstMessage];
chatsRef.current[chatId] = newChat;
startTransition(() => {
//just used to trigger UI update
});
}
});
return {
chats: chatsRef.current,
unsubscribeFromChat,

View File

@ -26,7 +26,7 @@ const createMockResponseMessageText = (): BusterChatMessage_text => ({
message_chunk: faker.lorem.sentence()
});
const createMockResponseMessageThought = (): BusterChatMessage_thought => {
export const createMockResponseMessageThought = (): BusterChatMessage_thought => {
const randomPillCount = faker.number.int(7);
const fourRandomPills: BusterChatMessage_thoughtPill[] = Array.from(
{ length: randomPillCount },
@ -45,7 +45,7 @@ const createMockResponseMessageThought = (): BusterChatMessage_thought => {
thought_secondary_title: faker.lorem.word(),
thought_pills: fourRandomPills,
hidden: false,
in_progress: false
in_progress: undefined
};
};
@ -70,12 +70,12 @@ export const MOCK_CHAT: BusterChat = {
request_message: createMockUserMessage(),
response_messages: [
createMockResponseMessageText(),
createMockResponseMessageThought(),
createMockResponseMessageThought()
// createMockResponseMessageThought(),
// createMockResponseMessageThought(),
// createMockResponseMessageThought(),
createMockResponseMessageFile(),
createMockResponseMessageFile()
// createMockResponseMessageFile(),
// createMockResponseMessageFile()
]
}
],