Merge branch 'big-nate/bus-939-create-new-structure-for-chats' into evals

This commit is contained in:
Nate Kelley 2025-03-11 14:02:46 -06:00
commit 2d99ad32b7
No known key found for this signature in database
GPG Key ID: FD90372AB8D98B4F
29 changed files with 167 additions and 76 deletions

View File

@ -61,11 +61,23 @@ export const useGetListLogs = (
});
};
export const useGetChat = (params: Parameters<typeof getChat>[0]) => {
const queryFn = useMemoizedFn(async () => {
return await getChat(params).then((chat) => {
console.log('TODO move this to put message in a better spot');
return updateChatToIChat(chat, true).iChat;
export const useGetChat = <TData = IBusterChat>(
params: Parameters<typeof getChat>[0],
select?: (chat: IBusterChat) => TData
) => {
const queryClient = useQueryClient();
const queryFn = useMemoizedFn(() => {
return getChat(params).then((chat) => {
const { iChat, iChatMessages } = updateChatToIChat(chat, false);
iChat.message_ids.forEach((messageId) => {
queryClient.setQueryData(
queryKeys.chatsMessages(messageId).queryKey,
iChatMessages[messageId]
);
});
return iChat;
});
});
@ -75,10 +87,11 @@ export const useGetChat = (params: Parameters<typeof getChat>[0]) => {
enabled: !!params.id
});
return useQuery<IBusterChat, RustApiError>({
return useQuery({
...queryKeys.chatsGetChat(params.id),
queryKey: queryKeys.chatsGetChat(params.id).queryKey,
enabled: !!params.id
enabled: !!params.id,
queryFn,
select
});
};

View File

@ -16,20 +16,12 @@ import {
import { useMemoizedFn } from '@/hooks';
import { QueryClient, useQueryClient } from '@tanstack/react-query';
import { queryKeys } from '@/api/query_keys';
import type {
UsersFavoritePostPayload,
UserFavoriteDeletePayload,
UserUpdateFavoritesPayload,
UserRequestUserListPayload
} from '@/api/request_interfaces/user/interfaces';
import type { UserRequestUserListPayload } from '@/api/request_interfaces/user/interfaces';
export const useGetMyUserInfo = () => {
const queryFn = useMemoizedFn(async () => {
return getMyUserInfo();
});
return useQuery({
...queryKeys.userGetUserMyself,
queryFn,
queryFn: getMyUserInfo,
enabled: false //This is a server only query
});
};

View File

@ -18,7 +18,8 @@ const favoritesGetList = queryOptions<BusterUserFavorite[]>({
});
const userGetUserMyself = queryOptions<BusterUserResponse>({
queryKey: ['users', 'myself'] as const
queryKey: ['users', 'myself'] as const,
staleTime: 1000 * 60 * 60 // 1 hour
});
const userGetUser = (userId: string) =>

View File

@ -100,7 +100,7 @@ const DropdownMenuItem = React.forwardRef<
<DropdownMenuPrimitive.Item
ref={ref}
className={cn(
'relative flex cursor-pointer items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0',
'relative flex cursor-pointer items-center gap-2 rounded-sm px-2 py-1.5 text-base outline-none select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0',
'focus:bg-item-hover focus:text-foreground',
inset && 'pl-8',
truncate && 'overflow-hidden',

View File

@ -1,5 +1,6 @@
import type { Meta, StoryObj } from '@storybook/react';
import { AppPageLayout } from './AppPageLayout';
import React from 'react';
const meta: Meta<typeof AppPageLayout> = {
title: 'UI/Layouts/AppPageLayout',
@ -53,6 +54,7 @@ export const LongContent: Story = {
args: {
header: <div className="bg-gray-100">Header Content</div>,
scrollable: true,
headerBorderVariant: 'ghost',
children: (
<>
{Array.from({ length: 100 }, (_, i) => (

View File

@ -39,7 +39,14 @@ export const AppPageLayout: React.FC<
{header}
</AppPageLayoutHeader>
)}
<AppPageLayoutContent scrollable={scrollable}>{children}</AppPageLayoutContent>
<AppPageLayoutContent className="scroll-shadow-container" scrollable={scrollable}>
{header && scrollable && headerBorderVariant === 'ghost' && (
<div className="scroll-header"></div>
)}
{children}
</AppPageLayoutContent>
</div>
);
};

View File

@ -3,7 +3,7 @@ import React from 'react';
import { cva, type VariantProps } from 'class-variance-authority';
const headerVariants = cva(
'bg-page-background flex max-h-[38px] min-h-[38px] items-center justify-between gap-x-2.5 ',
'bg-page-background flex max-h-[38px] min-h-[38px] items-center justify-between gap-x-2.5 relative z-10',
{
variants: {
sizeVariant: {

View File

@ -70,7 +70,7 @@ const AppMarkdownBase: React.FC<{
}, []);
return (
<div className={cn(styles.container, className)}>
<div className={cn(styles.container, 'flex flex-col gap-1.5', className)}>
<ReactMarkdown
remarkPlugins={[remarkGfm]}
skipHtml={true}

View File

@ -35,7 +35,11 @@ export const CustomParagraph: React.FC<
> = ({ children, markdown, showLoader, ...rest }) => {
if (Array.isArray(children)) {
return (
<p className={cn('leading-1.3', showLoader && 'animate-in fade-in duration-700')}>
<p
className={cn(
'leading-1.3 text-size-inherit!',
showLoader && 'animate-in fade-in duration-700'
)}>
{children}
</p>
);
@ -48,7 +52,13 @@ export const CustomParagraph: React.FC<
}
return (
<p className={cn('leading-1.3', showLoader && 'animate-in fade-in duration-700')}>{children}</p>
<p
className={cn(
'leading-1.3 text-size-inherit!',
showLoader && 'animate-in fade-in duration-700'
)}>
{children}
</p>
);
};
@ -112,7 +122,11 @@ export const CustomListItem: React.FC<
} & ExtraPropsExtra
> = ({ children, markdown, showLoader, ...rest }) => {
return (
<li className={cn('leading-1.3', showLoader && 'animate-in fade-in duration-700')}>
<li
className={cn(
'leading-1.3 list-inside list-disc',
showLoader && 'animate-in fade-in duration-700'
)}>
{children}
</li>
);

View File

@ -38,13 +38,13 @@ export const useChatUpdate = () => {
const options = queryKeys.chatsMessages(newMessageConfig.id);
const queryKey = options.queryKey;
const currentData = queryClient.getQueryData(queryKey);
const iChatMessage = create(currentData!, (draft) => {
if (currentData) {
const iChatMessage = create(currentData, (draft) => {
Object.assign(draft, newMessageConfig);
});
queryClient.setQueryData(queryKey, iChatMessage);
}
}
);
return {

View File

@ -107,6 +107,10 @@ export const useBusterNewChat = () => {
const onFollowUpChat = useMemoizedFn(
async ({ prompt, chatId }: { prompt: string; chatId: string }) => {
busterSocket.once({
route: '/chats/post:initializeChat',
callback: initializeNewChatCallback
});
await busterSocket.emitAndOnce({
emitEvent: {
route: '/chats/post',

View File

@ -191,7 +191,7 @@ export const updateReasoningMessage = (
Object.assign(fileContentDraft, existingFile?.file || {});
fileContentDraft.text = newFile.file.text_chunk
? (existingFile?.file?.text || '') + newFile.file.text_chunk
: (newFile.file.text ?? existingFile?.file?.text);
: (existingFile?.file?.text ?? newFile.file.text); //we are going to ignore newfile text in favor of existing... this is because Dallin is having a tough time keep yaml in order
fileContentDraft.modified =
newFile.file.modified ?? existingFile?.file?.modified;
});

View File

@ -37,10 +37,12 @@ export const useChatStreamMessage = () => {
const onUpdateChatMessageTransition = useMemoizedFn(
(chatMessage: Parameters<typeof onUpdateChatMessage>[0]) => {
const currentChatMessage = chatRefMessages.current[chatMessage.id];
const currentChatMessage = chatRefMessages.current[chatMessage.id] || {};
const iChatMessage: IBusterChatMessage = create(currentChatMessage, (draft) => {
Object.assign(draft || {}, chatMessage);
if (chatMessage.id) draft.id = chatMessage.id;
if (chatMessage.id) {
draft.id = chatMessage.id;
}
})!;
chatRefMessages.current[chatMessage.id] = iChatMessage;
@ -79,14 +81,17 @@ export const useChatStreamMessage = () => {
});
const initializeNewChatCallback = useMemoizedFn((d: BusterChat) => {
const hasMultipleMessages = d.message_ids.length > 1;
const { iChat, iChatMessages } = updateChatToIChat(d, true);
chatRef.current[iChat.id] = iChat;
normalizeChatMessage(iChatMessages);
onUpdateChat(iChat);
if (!hasMultipleMessages) {
onChangePage({
route: BusterRoutes.APP_CHAT_ID,
chatId: iChat.id
});
}
});
const replaceMessageCallback = useMemoizedFn(
@ -106,9 +111,12 @@ export const useChatStreamMessage = () => {
const _generatingTitleCallback = useMemoizedFn((_: null, newData: ChatEvent_GeneratingTitle) => {
const { chat_id } = newData;
const updatedChat = updateChatTitle(chatRef.current[chat_id], newData);
const currentChat = chatRef.current[chat_id];
if (currentChat) {
const updatedChat = updateChatTitle(currentChat, newData);
chatRef.current[chat_id] = updatedChat;
onUpdateChat(updatedChat);
}
});
const _generatingResponseMessageCallback = useMemoizedFn(

View File

@ -1,10 +1,11 @@
'use client';
import React from 'react';
import { useChatIndividualContextSelector } from '@/layouts/ChatLayout/ChatContext';
import { useMessageIndividual } from '@/context/Chats';
import { ReasoningMessageSelector } from './ReasoningMessages';
import { BlackBoxMessage } from './ReasoningMessages/ReasoningBlackBoxMessage';
import { useGetChat } from '@/api/buster_rest/chats';
import { FileIndeterminateLoader } from '@/components/features/FileIndeterminateLoader';
interface ReasoningControllerProps {
chatId: string;
@ -12,12 +13,12 @@ interface ReasoningControllerProps {
}
export const ReasoningController: React.FC<ReasoningControllerProps> = ({ chatId, messageId }) => {
const hasChat = useChatIndividualContextSelector((state) => state.hasChat);
const { data: hasChat } = useGetChat({ id: chatId || '' }, (x) => !!x.id);
const reasoningMessageIds = useMessageIndividual(messageId, (x) => x?.reasoning_message_ids);
const isCompletedStream = useMessageIndividual(messageId, (x) => x?.isCompletedStream);
if (!hasChat || !reasoningMessageIds)
return <>ReasoningController: If you are seeing this there is probably an error...</>;
if (!hasChat || !reasoningMessageIds) return <FileIndeterminateLoader />;
return (
<div className="h-full flex-col space-y-2 overflow-y-auto p-5">

View File

@ -23,7 +23,11 @@ export const BarContainer: React.FC<{
/>
<div className={`mb-2 flex w-full flex-col space-y-2 overflow-hidden`}>
<TitleContainer title={title} secondaryTitle={secondaryTitle} />
<TitleContainer
title={title}
secondaryTitle={secondaryTitle}
isCompletedStream={isCompletedStream}
/>
{children}
</div>
</div>
@ -77,12 +81,14 @@ VerticalBar.displayName = 'VerticalBar';
const TitleContainer: React.FC<{
title: string;
secondaryTitle?: string;
}> = React.memo(({ title, secondaryTitle }) => {
isCompletedStream: boolean;
}> = React.memo(({ title, secondaryTitle, isCompletedStream }) => {
return (
<div className={cn('@container', 'flex w-full items-center space-x-1.5 overflow-hidden')}>
<AnimatedThoughtTitle title={title} type="default" />
<AnimatedThoughtTitle title={title} type="default" isCompletedStream={isCompletedStream} />
<AnimatedThoughtTitle
title={secondaryTitle}
isCompletedStream={isCompletedStream}
type="tertiary"
className="secondary-text truncate"
/>
@ -96,22 +102,24 @@ const AnimatedThoughtTitle = React.memo(
({
title,
type,
isCompletedStream,
className = ''
}: {
title: string | undefined;
type: 'tertiary' | 'default';
className?: string;
isCompletedStream: boolean;
}) => {
const isSecondaryTitle = type === 'tertiary';
return (
<AnimatePresence initial={false} mode="wait">
<AnimatePresence initial={!isCompletedStream && isSecondaryTitle} mode="wait">
{title && (
<motion.div
className="flex"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ delay: 0.125 }}
transition={{ delay: isSecondaryTitle ? 0.5 : 0.125 }}
key={title}>
<Text
size="sm"

View File

@ -96,7 +96,7 @@ export const ReasoningMessageSelector: React.FC<ReasoningMessageSelectorProps> =
isCompletedStream={isCompletedStream}
title={title ?? ''}
secondaryTitle={secondary_title ?? ''}>
<AnimatePresence mode="wait">
<AnimatePresence mode="wait" initial={!isCompletedStream}>
<motion.div key={animationKey} {...itemAnimationConfig} className="overflow-hidden" layout>
<div className="min-h-[1px]">
<ReasoningMessage

View File

@ -3,7 +3,6 @@ import { ReasoningMessageProps } from '../ReasoningMessageSelector';
import { type BusterChatMessageReasoning_text } from '@/api/asset_interfaces/chat';
import { useMessageIndividual } from '@/context/Chats';
import { AppMarkdown } from '@/components/ui/typography/AppMarkdown';
import { StreamingMessage_Text } from '@/components/ui/streaming/StreamingMessage_Text';
export const ReasoningMessage_Text: React.FC<ReasoningMessageProps> = React.memo(
({ reasoningMessageId, messageId, isCompletedStream }) => {
@ -12,7 +11,14 @@ export const ReasoningMessage_Text: React.FC<ReasoningMessageProps> = React.memo
(x) => (x?.reasoning_messages[reasoningMessageId] as BusterChatMessageReasoning_text)?.message
)!;
return <AppMarkdown markdown={message} showLoader={!isCompletedStream} stripFormatting />;
return (
<AppMarkdown
markdown={message}
showLoader={!isCompletedStream}
className="text-text-secondary text-xs!"
stripFormatting
/>
);
}
);

View File

@ -16,7 +16,7 @@ export const ChatContainer = React.memo(() => {
return (
<AppPageLayout
header={<ChatHeader showScrollOverflow={showScrollOverflow} />}
header={<ChatHeader />}
headerBorderVariant="ghost"
className="flex h-full w-full min-w-[295px] flex-col">
<ChatContent chatContentRef={chatContentRef} />

View File

@ -2,7 +2,6 @@ import React from 'react';
import { MessageContainer } from '../MessageContainer';
import { ChatResponseMessageSelector } from './ChatResponseMessageSelector';
import { ChatResponseReasoning } from './ChatResponseReasoning';
import { ShimmerText } from '@/components/ui/typography/ShimmerText';
import { useMessageIndividual } from '@/context/Chats';
interface ChatResponseMessagesProps {

View File

@ -55,10 +55,12 @@ export const ChatResponseReasoning: React.FC<{
<motion.div
{...animations}
key={text}
className="mb-3.5 w-fit cursor-pointer"
className="mb-3.5 flex h-[14px] max-h-[14px] w-fit cursor-pointer items-center"
onClick={onClickReasoning}>
{isReasonginFileSelected ? (
<Text className="hover:underline">{text}</Text>
<Text className="text-text-secondary hover:text-text-default hover:underline">
{text}
</Text>
) : (
<ShimmerText text={text ?? ''} />
)}

View File

@ -11,7 +11,7 @@ export const ChatUserMessage: React.FC<{ requestMessage: BusterChatMessageReques
return (
<MessageContainer senderName={sender_name} senderId={sender_id} senderAvatar={sender_avatar}>
<Paragraph className="text-sm">{request}</Paragraph>
<Paragraph>{request}</Paragraph>
</MessageContainer>
);
}

View File

@ -5,17 +5,15 @@ import { ChatHeaderOptions } from './ChatHeaderOptions';
import { ChatHeaderTitle } from './ChatHeaderTitle';
import { useChatIndividualContextSelector } from '../../ChatContext';
export const ChatHeader: React.FC<{
showScrollOverflow: boolean;
}> = React.memo(({ showScrollOverflow }) => {
export const ChatHeader: React.FC<{}> = React.memo(({}) => {
const hasFile = useChatIndividualContextSelector((state) => state.hasFile);
const chatTitle = useChatIndividualContextSelector((state) => state.chatTitle);
if (!hasFile && !chatTitle) return null;
if (!hasFile || !chatTitle) return null;
return (
<>
<ChatHeaderTitle />
<ChatHeaderTitle chatTitle={chatTitle} />
<ChatHeaderOptions />
</>
);

View File

@ -3,7 +3,6 @@
import { Text } from '@/components/ui/typography';
import React from 'react';
import { AnimatePresence, motion } from 'framer-motion';
import { useChatIndividualContextSelector } from '../../ChatContext';
const animation = {
initial: { opacity: 0 },
@ -12,8 +11,10 @@ const animation = {
transition: { duration: 0.25 }
};
export const ChatHeaderTitle: React.FC<{}> = React.memo(() => {
const chatTitle = useChatIndividualContextSelector((state) => state.chatTitle);
export const ChatHeaderTitle: React.FC<{
chatTitle: string;
}> = React.memo(({ chatTitle }) => {
if (!chatTitle) return <div></div>;
return (
<AnimatePresence mode="wait" initial={false}>

View File

@ -20,8 +20,12 @@ const useChatIndividualContext = ({
const selectedFileType = selectedFile?.type;
//CHAT
const { data: chat } = useGetChat({ id: chatId || '' });
const hasChat = !!chatId && !!chat;
const { data: chat } = useGetChat({ id: chatId || '' }, (x) => ({
title: x.title,
message_ids: x.message_ids,
id: x.id
}));
const hasChat = !!chatId && !!chat?.id;
const chatTitle = chat?.title;
const chatMessageIds = chat?.message_ids ?? [];

View File

@ -11,7 +11,7 @@ export const useAutoChangeLayout = ({
lastMessageId: string;
onSetSelectedFile: (file: SelectedFile) => void;
}) => {
const hasSeeningReasoningPage = useRef(false); //used when there is a delay in page load
const previousLastMessageId = useRef<string | null>(null);
const reasoningMessagesLength = useMessageIndividual(
lastMessageId,
(x) => x?.reasoning_message_ids?.length || 0
@ -21,9 +21,10 @@ export const useAutoChangeLayout = ({
//change the page to reasoning file if we get a reasoning message
useEffect(() => {
if (!isCompletedStream && !hasSeeningReasoningPage.current && hasReasoning) {
hasSeeningReasoningPage.current = true;
if (!isCompletedStream && hasReasoning && previousLastMessageId.current !== lastMessageId) {
// hasSeeningReasoningPage.current = true;
onSetSelectedFile({ id: lastMessageId, type: 'reasoning' });
previousLastMessageId.current = lastMessageId;
}
}, [isCompletedStream, hasReasoning]);
}, [isCompletedStream, hasReasoning, lastMessageId]);
};

View File

@ -4,6 +4,7 @@ import { create } from 'mutative';
import omit from 'lodash/omit';
import { BusterMetric, IBusterMetric } from '@/api/asset_interfaces/metric';
import { createDefaultChartConfig } from './messageAutoChartHandler';
import last from 'lodash/last';
const chatUpgrader = (chat: BusterChat, { isNewChat }: { isNewChat: boolean }): IBusterChat => {
return {
@ -20,7 +21,7 @@ const chatMessageUpgrader = (
return messageIds.reduce(
(acc, messageId) => {
acc[messageId] = create(message[messageId] as IBusterChatMessage, (draft) => {
draft.isCompletedStream = streamingMessageId !== messageId;
draft.isCompletedStream = !streamingMessageId || streamingMessageId !== messageId;
return draft;
});
return acc;
@ -37,7 +38,7 @@ export const updateChatToIChat = (
const iChatMessages = chatMessageUpgrader(
chat.message_ids,
chat.messages,
isNewChat ? chat.message_ids[0] : undefined
isNewChat ? last(chat.message_ids) : undefined
);
return {
iChat,

View File

@ -5,3 +5,31 @@
display: none;
}
}
// This approach has very limited browser support
:root {
--globalScrollTimeline: none; // Define the timeline globally
}
.scroll-shadow-container {
position: relative;
scroll-timeline-name: --globalScrollTimeline;
@apply relative;
.scroll-header {
@apply fixed top-[30px] right-0 left-0 h-2 w-full;
animation: shadowAnimation linear;
animation-range: 0px 125px;
animation-timeline: --globalScrollTimeline;
animation-fill-mode: forwards;
}
@keyframes shadowAnimation {
from {
box-shadow: 0 0 0 rgba(0, 0, 0, 0);
}
to {
box-shadow: 0px 1px 8px 0px #00000029;
}
}
}

View File

@ -45,5 +45,5 @@ body {
}
p {
@apply leading-1.3 text-base;
@apply leading-1.3;
}

View File

@ -40,6 +40,7 @@
--text-3xl--line-height: 1;
--text-4xl: 30px;
--text-4xl--line-height: 1;
--text-size-inherit: inherit;
--text-icon-size: 16px;
--text-icon-size--line-height: 1.05;