reasoning file click

This commit is contained in:
Nate Kelley 2025-02-08 15:40:41 -07:00
parent 75873c2ef5
commit 4ef095ceba
No known key found for this signature in database
GPG Key ID: FD90372AB8D98B4F
11 changed files with 369 additions and 165 deletions

View File

@ -6,7 +6,7 @@ import type { IBusterChatMessage } from '@/context/Chats/interfaces';
export const ChatMessageBlock: React.FC<{
message: IBusterChatMessage;
}> = React.memo(({ message }) => {
const { request_message, response_messages, id, isCompletedStream } = message;
const { request_message, response_messages, id, isCompletedStream, reasoning } = message;
return (
<div className={'flex flex-col space-y-3.5 py-2 pl-4 pr-3'} id={id}>
@ -14,6 +14,8 @@ export const ChatMessageBlock: React.FC<{
<ChatResponseMessages
responseMessages={response_messages}
isCompletedStream={isCompletedStream}
reasoningMessages={reasoning}
messageId={id}
/>
</div>
);

View File

@ -1,7 +1,9 @@
import React from 'react';
import React, { useMemo } from 'react';
import { ChatResponseMessage_File } from './ChatResponseMessage_File';
import { StreamingMessage_Text } from '@appComponents/Streaming/StreamingMessage_Text';
import type { BusterChatMessage_text, BusterChatMessageResponse } from '@/api/asset_interfaces';
import { createStyles } from 'antd-style';
import { useMemoizedFn } from 'ahooks';
export interface ChatResponseMessageProps {
responseMessage: BusterChatMessageResponse;
@ -32,11 +34,74 @@ export const ChatResponseMessageSelector: React.FC<ChatResponseMessageSelectorPr
}) => {
const messageType = responseMessage.type;
const ChatResponseMessage = ChatResponseMessageRecord[messageType];
const { cx, styles } = useStyles();
const typeClassRecord: Record<BusterChatMessageResponse['type'], string> = useMemo(() => {
return {
text: cx(styles.textCard, 'text-card'),
file: cx(styles.fileCard, 'file-card')
};
}, []);
const getContainerClass = useMemoizedFn((item: BusterChatMessageResponse) => {
return typeClassRecord[item.type];
});
return (
<ChatResponseMessage
responseMessage={responseMessage}
isCompletedStream={isCompletedStream}
isLastMessageItem={isLastMessageItem}
/>
<div key={responseMessage.id} className={getContainerClass(responseMessage)}>
<ChatResponseMessage
responseMessage={responseMessage}
isCompletedStream={isCompletedStream}
isLastMessageItem={isLastMessageItem}
/>
<VerticalDivider />
</div>
);
};
const VerticalDivider: React.FC<{ className?: string }> = React.memo(({ className }) => {
const { cx, styles } = useStyles();
return <div className={cx(styles.verticalDivider, 'vertical-divider', className)} />;
});
VerticalDivider.displayName = 'VerticalDivider';
const useStyles = createStyles(({ token, css }) => ({
textCard: css`
margin-bottom: 14px;
&:has(+ .text-card) {
margin-bottom: 8px;
}
.vertical-divider {
display: none;
}
`,
fileCard: css`
&:has(+ .text-card) {
.vertical-divider {
opacity: 0;
}
}
&:has(+ .file-card) {
.vertical-divider {
opacity: 1;
}
margin-bottom: 1px;
}
&:last-child {
.vertical-divider {
opacity: 0;
}
}
`,
verticalDivider: css`
transition: opacity 0.2s ease-in-out;
height: 9px;
width: 0.5px;
margin: 3px 0 3px 16px;
background: ${token.colorTextTertiary};
`
}));

View File

@ -1,46 +1,56 @@
import React, { useMemo } from 'react';
import type { BusterChatMessageResponse } from '@/api/asset_interfaces';
import type {
BusterChatMessage_text,
BusterChatMessageReasoning,
BusterChatMessageResponse
} from '@/api/asset_interfaces';
import { MessageContainer } from '../MessageContainer';
import { useMemoizedFn } from 'ahooks';
import { ChatResponseMessageSelector } from './ChatResponseMessageSelector';
import { createStyles } from 'antd-style';
import { ChatResponseReasoning } from './ChatResponseReasoning';
interface ChatResponseMessagesProps {
responseMessages: BusterChatMessageResponse[];
isCompletedStream: boolean;
reasoningMessages: BusterChatMessageReasoning[];
messageId: string;
}
export const ChatResponseMessages: React.FC<ChatResponseMessagesProps> = React.memo(
({ responseMessages, isCompletedStream }) => {
const { styles, cx } = useStyles();
const firstResponseMessage = responseMessages[0];
const restResponseMessages = responseMessages.slice(1);
({ responseMessages, reasoningMessages, isCompletedStream, messageId }) => {
const firstResponseMessage = responseMessages[0] as BusterChatMessage_text;
const restResponseMessages = useMemo(() => {
if (!firstResponseMessage) return [];
return responseMessages.slice(1);
}, [firstResponseMessage, responseMessages]);
const lastMessageIndex = responseMessages.length - 1;
const typeClassRecord: Record<BusterChatMessageResponse['type'], string> = useMemo(() => {
return {
text: cx(styles.textCard, 'text-card'),
file: cx(styles.fileCard, 'file-card')
};
}, []);
const getContainerClass = useMemoizedFn((item: BusterChatMessageResponse) => {
return typeClassRecord[item.type];
});
return (
<MessageContainer className="flex w-full flex-col overflow-hidden">
{responseMessages.map((responseMessage, index) => (
<div key={responseMessage.id} className={getContainerClass(responseMessage)}>
<ChatResponseMessageSelector
responseMessage={responseMessage}
isCompletedStream={isCompletedStream}
isLastMessageItem={index === lastMessageIndex}
/>
<VerticalDivider />
</div>
{firstResponseMessage && (
<ChatResponseMessageSelector
key={firstResponseMessage.id}
responseMessage={firstResponseMessage}
isCompletedStream={isCompletedStream}
isLastMessageItem={false}
/>
)}
{firstResponseMessage && (
<ChatResponseReasoning
reasoningMessages={reasoningMessages}
isCompletedStream={isCompletedStream}
messageId={messageId}
/>
)}
{restResponseMessages.map((responseMessage, index) => (
<ChatResponseMessageSelector
key={responseMessage.id}
responseMessage={responseMessage}
isCompletedStream={isCompletedStream}
isLastMessageItem={index === lastMessageIndex}
/>
))}
</MessageContainer>
);
@ -48,52 +58,3 @@ export const ChatResponseMessages: React.FC<ChatResponseMessagesProps> = React.m
);
ChatResponseMessages.displayName = 'ChatResponseMessages';
const VerticalDivider: React.FC<{ className?: string }> = React.memo(({ className }) => {
const { cx, styles } = useStyles();
return <div className={cx(styles.verticalDivider, 'vertical-divider', className)} />;
});
VerticalDivider.displayName = 'VerticalDivider';
const useStyles = createStyles(({ token, css }) => ({
textCard: css`
margin-bottom: 14px;
&:has(+ .text-card) {
margin-bottom: 8px;
}
.vertical-divider {
display: none;
}
`,
fileCard: css`
&:has(+ .text-card),
&:has(+ .hidden-card) {
.vertical-divider {
opacity: 0;
}
margin-bottom: 0px;
}
&:has(+ .thought-card) {
.vertical-divider {
opacity: 0;
}
margin-bottom: 0px;
}
&:last-child {
.vertical-divider {
opacity: 0;
}
}
`,
verticalDivider: css`
transition: opacity 0.2s ease-in-out;
height: 9px;
width: 0.5px;
margin: 3px 0 3px 16px;
background: ${token.colorTextTertiary};
`
}));

View File

@ -0,0 +1,175 @@
import { BusterChatMessageReasoning } from '@/api/asset_interfaces';
import React, { useEffect, useMemo, useState } from 'react';
import last from 'lodash/last';
import { ShimmerText } from '@/components/text';
import { useMemoizedFn } from 'ahooks';
import { motion } from 'framer-motion';
import { AnimatePresence } from 'framer-motion';
import { AppMaterialIcons, Text } from '@/components';
import { createStyles } from 'antd-style';
import { useChatLayoutContextSelector } from '../../../ChatLayoutContext';
export const ChatResponseReasoning: React.FC<{
reasoningMessages: BusterChatMessageReasoning[];
isCompletedStream: boolean;
messageId: string;
}> = React.memo(({ reasoningMessages, isCompletedStream, messageId }) => {
const lastMessage = last(reasoningMessages);
const onSetSelectedFile = useChatLayoutContextSelector((x) => x.onSetSelectedFile);
const selectedFileType = useChatLayoutContextSelector((x) => x.selectedFileType);
const isReasonginFileSelected = selectedFileType === 'reasoning';
const text = useMemo(() => {
if (!lastMessage) return null;
if (lastMessage.type === 'text') {
return lastMessage.message;
}
return lastMessage.thought_title;
}, [lastMessage]);
const getRandomThought = useMemoizedFn(() => {
return DEFAULT_THOUGHTS[Math.floor(Math.random() * DEFAULT_THOUGHTS.length)];
});
const onClickReasoning = useMemoizedFn(() => {
onSetSelectedFile({
type: 'reasoning',
id: messageId
});
});
const [thought, setThought] = useState(text || DEFAULT_THOUGHTS[0]);
useEffect(() => {
if (!isCompletedStream && !text) {
const randomInterval = Math.floor(Math.random() * 3000) + 1200;
const interval = setTimeout(() => {
setThought(getRandomThought());
}, randomInterval);
return () => clearTimeout(interval);
}
if (text) {
setThought(text);
}
}, [thought, isCompletedStream, text, getRandomThought]);
const animations = useMemo(() => {
return {
initial: { opacity: 0 },
animate: { opacity: 1 },
exit: { opacity: 0 }
};
}, []);
return (
<AnimatePresence initial={!isCompletedStream} mode="wait">
<motion.div {...animations} key={thought} className="mb-3.5 w-fit" onClick={onClickReasoning}>
<ShimmerTextWithIcon
text={thought}
isCompletedStream={isCompletedStream}
isSelected={isReasonginFileSelected}
/>
</motion.div>
</AnimatePresence>
);
});
ChatResponseReasoning.displayName = 'ChatThoughts';
const DEFAULT_THOUGHTS = [
'Thinking through next steps...',
'Looking through context...',
'Reflecting on the instructions...',
'Analyzing available actions',
'Reviewing the objective...',
'Deciding feasible options...',
'Sorting out some details...',
'Exploring other possibilities...',
'Confirming things....',
'Mapping information across files...',
'Making a few edits...',
'Filling out arguments...',
'Double-checking the logic...',
'Validating my approach...',
'Looking at a few edge cases...',
'Ensuring everything aligns...',
'Polishing the details...',
'Making some adjustments...',
'Writing out arguments...',
'Mapping trends and patterns...',
'Re-evaluating this step...',
'Updating parameters...',
'Evaluating available data...',
'Reviewing all parameters...',
'Processing relevant info...',
'Aligning with user request...',
'Gathering necessary details...',
'Sorting through options...',
'Editing my system logic...',
'Cross-checking references...',
'Validating my approach...',
'Rewriting operational details...',
'Mapping new information...',
'Adjusting priorities & approach...',
'Revisiting earlier inputs...',
'Finalizing plan details...'
];
const ShimmerTextWithIcon = React.memo(
({
text,
isCompletedStream,
isSelected
}: {
text: string;
isCompletedStream: boolean;
isSelected: boolean;
}) => {
const { cx, styles } = useStyles();
if (isCompletedStream) {
return (
<div
className={cx(
styles.iconContainerCompleted,
styles.iconContainer,
'flex w-fit items-center gap-1',
isSelected && 'is-selected'
)}>
<div>
<AppMaterialIcons icon="stars" />
</div>
<Text type="inherit">{text}</Text>
</div>
);
}
return (
<div className={cx(styles.iconContainer, 'flex items-center gap-1')}>
<div className={cx(styles.icon)}>
<AppMaterialIcons icon="stars" />
</div>
<ShimmerText text={text} />
</div>
);
}
);
ShimmerTextWithIcon.displayName = 'ShimmerTextWithIcon';
const useStyles = createStyles(({ token, css }) => ({
iconContainerCompleted: css`
color: ${token.colorIcon};
&:hover {
color: ${token.colorText};
}
&.is-selected {
color: ${token.colorText};
}
`,
iconContainer: css`
cursor: pointer;
`,
icon: css`
color: ${token.colorIcon};
`
}));

View File

@ -1,72 +0,0 @@
import { BusterChatMessageReasoning } from '@/api/asset_interfaces';
import React, { useMemo } from 'react';
import last from 'lodash/last';
import { ShimmerText } from '@/components/text';
export const ChatThoughts: React.FC<{
reasoningMessages: BusterChatMessageReasoning[];
}> = React.memo(({ reasoningMessages }) => {
const lastMessage = last(reasoningMessages);
const lastMessageTest = useMemo(() => {
if (!lastMessage) return null;
if (lastMessage.type === 'text') {
return lastMessage;
}
return lastMessage.thought_title;
}, [lastMessage]);
const hasLastMessage = !!lastMessage || !!lastMessageTest;
if (!hasLastMessage) return null;
return <div>ChatThoughts</div>;
});
ChatThoughts.displayName = 'ChatThoughts';
const DEFAULT_THOUGHTS = [
'Thinking through next steps...',
'Looking through context...',
'Reflecting on the instructions...',
'Analyzing available actions',
'Reviewing the objective...',
'Deciding feasible options...',
'Sorting out some details...',
'Exploring other possibilities...',
'Confirming things....',
'Mapping information across files...',
'Making a few edits...',
'Filling out arguments...',
'Double-checking the logic...',
'Validating my approach...',
'Looking at a few edge cases...',
'Ensuring everything aligns...',
'Polishing the details...',
'Making some adjustments...',
'Writing out arguments...',
'Mapping trends and patterns...',
'Re-evaluating this step...',
'Updating parameters...',
'Evaluating available data...',
'Reviewing all parameters...',
'Processing relevant info...',
'Aligning with user request...',
'Gathering necessary details...',
'Sorting through options...',
'Editing my system logic...',
'Cross-checking references...',
'Validating my approach...',
'Rewriting operational details...',
'Mapping new information...',
'Adjusting priorities & approach...',
'Revisiting earlier inputs...',
'Finalizing plan details...'
];
const RandomThoughts = React.memo(() => {
const randomThought = DEFAULT_THOUGHTS[Math.floor(Math.random() * DEFAULT_THOUGHTS.length)];
return <div>{randomThought}</div>;
});
RandomThoughts.displayName = 'RandomThoughts';

View File

@ -3,7 +3,7 @@ import {
createContext,
useContextSelector
} from '@fluentui/react-context-selector';
import React, { PropsWithChildren, useState, useTransition } from 'react';
import React, { PropsWithChildren, useTransition } from 'react';
import type { SelectedFile } from '../interfaces';
import type { ChatSplitterProps } from '../ChatLayout';
import { useMemoizedFn } from 'ahooks';
@ -51,7 +51,7 @@ export const useChatLayout = ({
const fileType = file.type;
const fileId = file.id;
const route =
isChatView && chatId
isChatView && chatId !== undefined
? createChatAssetRoute({ chatId, assetId: fileId, type: fileType })
: createFileRoute({ assetId: fileId, type: fileType });

View File

@ -1,6 +1,7 @@
import type { ThoughtFileType, FileType } from '@/api/asset_interfaces';
export const isOpenableFile = (type: ThoughtFileType): type is FileType => {
const validTypes: FileType[] = ['metric', 'dashboard'];
return validTypes.includes(type as FileType);
const OPENABLE_FILES = new Set<string>(['metric', 'dashboard', 'reasoning']);
export const isOpenableFile = (type: ThoughtFileType): boolean => {
return OPENABLE_FILES.has(type);
};

View File

@ -36,7 +36,5 @@ export const useSelectedFileByParams = () => {
return 'chat';
}, [metricId, collectionId, datasetId, dashboardId, chatId]);
console.log(selectedFile, selectedLayout, chatId);
return { selectedFile, selectedLayout, chatId };
};

View File

@ -90,6 +90,7 @@ export const MOCK_CHAT: BusterChat = {
response_messages: [
createMockResponseMessageText(),
createMockResponseMessageFile(),
createMockResponseMessageFile(),
createMockResponseMessageText(),
createMockResponseMessageText()
]

View File

@ -4,7 +4,13 @@ import { useMemoizedFn } from 'ahooks';
import { BusterChat } from '@/api/asset_interfaces';
import { IBusterChat } from '../interfaces';
import { chatUpgrader } from './helpers';
import { MOCK_CHAT } from './MOCK_CHAT';
import {
createMockResponseMessageFile,
createMockResponseMessageText,
createMockResponseMessageThought,
MOCK_CHAT
} from './MOCK_CHAT';
import { useHotkeys } from 'react-hotkeys-hook';
export const useChatSubscriptions = ({
chatsRef,
@ -47,6 +53,73 @@ export const useChatSubscriptions = ({
// });
});
useHotkeys('t', () => {
const newThoughts = createMockResponseMessageThought();
const myChat = {
...chatsRef.current[MOCK_CHAT.id]!,
messages: [
{
...chatsRef.current[MOCK_CHAT.id]!.messages[0],
reasoning: [...chatsRef.current[MOCK_CHAT.id]!.messages[0].reasoning, newThoughts],
isCompletedStream: false
}
]
};
chatsRef.current[MOCK_CHAT.id] = myChat;
startTransition(() => {
// Create a new reference to trigger React update
chatsRef.current = { ...chatsRef.current };
});
});
useHotkeys('m', () => {
const newTextMessage = createMockResponseMessageText();
const myChat = {
...chatsRef.current[MOCK_CHAT.id]!,
messages: [
{
...chatsRef.current[MOCK_CHAT.id]!.messages[0],
response_messages: [
...chatsRef.current[MOCK_CHAT.id]!.messages[0]!.response_messages,
newTextMessage
],
isCompletedStream: false
}
]
};
chatsRef.current[MOCK_CHAT.id] = myChat;
startTransition(() => {
chatsRef.current = { ...chatsRef.current };
});
});
useHotkeys('f', () => {
const newFileMessage = createMockResponseMessageFile();
const myChat = {
...chatsRef.current[MOCK_CHAT.id]!,
messages: [
{
...chatsRef.current[MOCK_CHAT.id]!.messages[0],
response_messages: [
...chatsRef.current[MOCK_CHAT.id]!.messages[0]!.response_messages,
newFileMessage
],
isCompletedStream: false
}
]
};
chatsRef.current[MOCK_CHAT.id] = myChat;
startTransition(() => {
chatsRef.current = { ...chatsRef.current };
});
});
return {
unsubscribeFromChat,
subscribeToChat

View File

@ -24,7 +24,7 @@ export enum BusterAppRoutes {
//NEW CHAT
APP_CHAT_ID = '/app/chat/:chatId',
APP_CHAT_ID_REASONING_ID = '/app/chat/:chatId/reasoning/:reasoningId',
APP_CHAT_ID_REASONING_ID = '/app/chat/:chatId/reasoning/:messageId',
APP_CHAT_ID_METRIC_ID = '/app/chat/:chatId/metric/:metricId',
APP_CHAT_ID_COLLECTION_ID = '/app/chat/:chatId/collection/:collectionId',
APP_CHAT_ID_DASHBOARD_ID = '/app/chat/:chatId/dashboard/:dashboardId',