update reasoning page

This commit is contained in:
Nate Kelley 2025-08-29 20:38:10 -06:00
parent 5d54f608b5
commit b55dd2956a
No known key found for this signature in database
GPG Key ID: FD90372AB8D98B4F
14 changed files with 88 additions and 264 deletions

View File

@ -1,10 +1,9 @@
'use client';
import * as React from 'react';
import * as SheetPrimitive from '@radix-ui/react-dialog';
import { Xmark } from '../icons';
import type * as React from 'react';
import { cn } from '@/lib/utils';
import { Xmark } from '../icons';
function Sheet({ ...props }: React.ComponentProps<typeof SheetPrimitive.Root>) {
return <SheetPrimitive.Root data-slot="sheet" {...props} />;
@ -63,7 +62,8 @@ function SheetContent({
'data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom right-2 bottom-2 left-2 h-auto rounded border-t',
className
)}
{...props}>
{...props}
>
{children}
{/* <SheetPrimitive.Close className="ring-offset-background focus:ring-ring data-[state=open]:bg-secondary absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none">
<div className="size-4">
@ -127,5 +127,5 @@ export {
SheetHeader,
SheetFooter,
SheetTitle,
SheetDescription
SheetDescription,
};

View File

@ -1,36 +1,36 @@
import type { Meta, StoryObj } from '@storybook/nextjs';
import { Sheet } from './Sheets';
import type { Meta, StoryObj } from '@storybook/react-vite';
import { Button } from '../buttons/Button';
import { Sheet } from './Sheets';
const meta: Meta<typeof Sheet> = {
title: 'UI/sheet/Sheet',
component: Sheet,
parameters: {
layout: 'centered'
layout: 'centered',
},
tags: ['autodocs'],
argTypes: {
side: {
control: { type: 'select' },
options: ['top', 'right', 'bottom', 'left']
options: ['top', 'right', 'bottom', 'left'],
},
closeStyle: {
control: { type: 'select' },
options: ['collapse', 'close', 'none']
options: ['collapse', 'close', 'none'],
},
trigger: {
control: false
control: false,
},
children: {
control: false
control: false,
},
header: {
control: false
control: false,
},
footer: {
control: false
}
}
control: false,
},
},
};
export default meta;
@ -52,6 +52,6 @@ export const WithHeader: Story = {
<p key={index}>Sheet content with a structured header above {index}</p>
))}
</div>
)
}
),
},
};

View File

@ -34,7 +34,7 @@ export const useIsAssetFileChanged = () => {
return reportParams;
}
const _exhaustiveCheck: 'chat' | 'collection' = assetType;
const _exhaustiveCheck: 'chat' | 'collection' | 'reasoning' = assetType;
return {
isFileChanged: false,

View File

@ -1,10 +1,10 @@
'use client';
import React from 'react';
import type { BusterChatMessage } from '@/api/asset_interfaces/chat';
import { useGetChatMessage } from '@/api/buster_rest/chats';
import { ChatResponseMessages } from './ChatResponseMessages';
import { ChatUserMessage } from './ChatUserMessage';
import type { BusterChatMessage } from '@/api/asset_interfaces/chat';
// Stable selector functions to prevent unnecessary re-renders
const selectMessageExists = (message: BusterChatMessage | undefined) => !!message?.id;
@ -17,13 +17,13 @@ export const ChatMessageBlock: React.FC<{
messageIndex: number;
}> = React.memo(({ messageId, chatId, messageIndex }) => {
const { data: messageExists } = useGetChatMessage(messageId, {
select: selectMessageExists
select: selectMessageExists,
});
const { data: requestMessage } = useGetChatMessage(messageId, {
select: selectRequestMessage
select: selectRequestMessage,
});
const { data: isStreamFinished = true } = useGetChatMessage(messageId, {
select: selectIsCompleted
select: selectIsCompleted,
});
if (!messageExists) return null;

View File

@ -1,9 +1,9 @@
import type { Meta, StoryObj } from '@storybook/nextjs';
import { http, HttpResponse } from 'msw';
import type { Meta, StoryObj } from '@storybook/react-vite';
import { HttpResponse, http } from 'msw';
import type { BusterChatResponseMessage_file } from '@/api/asset_interfaces/chat/chatMessageInterfaces';
import { ChatResponseMessage_DashboardFile } from './ChatResponseMessage_DashboardFile';
import { BASE_URL } from '@/api/buster_rest/config';
import { BASE_URL } from '@/api/config';
import { generateMockDashboard } from '@/mocks/MOCK_DASHBOARD';
import { ChatResponseMessage_DashboardFile } from './ChatResponseMessage_DashboardFile';
const mockResponseMessage: BusterChatResponseMessage_file = {
id: 'dashboard-response-1',
@ -16,9 +16,9 @@ const mockResponseMessage: BusterChatResponseMessage_file = {
{
status: 'completed',
message: 'Dashboard loaded successfully',
timestamp: Date.now()
}
]
timestamp: Date.now(),
},
],
};
const { response: mockDashboardResponse } = generateMockDashboard(
@ -40,25 +40,25 @@ const meta: Meta<typeof ChatResponseMessage_DashboardFile> = {
// You can handle different logic based on version_number if needed
// For now, returning the same mock response regardless of version
return HttpResponse.json(mockDashboardResponse);
})
]
}
}),
],
},
},
tags: ['autodocs'],
argTypes: {
isStreamFinished: {
control: 'boolean',
description: 'Whether the stream has completed'
description: 'Whether the stream has completed',
},
responseMessage: {
control: false,
description: 'The dashboard file response message'
description: 'The dashboard file response message',
},
isSelectedFile: {
control: 'boolean',
description: 'Whether this file is currently selected'
}
}
description: 'Whether this file is currently selected',
},
},
};
export default meta;
@ -68,24 +68,24 @@ export const Default: Story = {
args: {
isStreamFinished: true,
responseMessage: mockResponseMessage,
isSelectedFile: false
}
isSelectedFile: false,
},
};
export const Selected: Story = {
args: {
isStreamFinished: true,
responseMessage: mockResponseMessage,
isSelectedFile: true
}
isSelectedFile: true,
},
};
export const StreamingInProgress: Story = {
args: {
isStreamFinished: false,
responseMessage: mockResponseMessage,
isSelectedFile: false
}
isSelectedFile: false,
},
};
export const LoadingState: Story = {
@ -99,15 +99,15 @@ export const LoadingState: Story = {
// Delay response to show loading state
await new Promise((resolve) => setTimeout(resolve, 2000));
return HttpResponse.json(mockDashboardResponse);
})
]
}
}),
],
},
},
args: {
isStreamFinished: true,
responseMessage: mockResponseMessage,
isSelectedFile: false
}
isSelectedFile: false,
},
};
export const ErrorState: Story = {
@ -119,15 +119,15 @@ export const ErrorState: Story = {
const versionNumber = url.searchParams.get('version_number');
return HttpResponse.json({ error: 'Dashboard not found' }, { status: 404 });
})
]
}
}),
],
},
},
args: {
isStreamFinished: true,
responseMessage: mockResponseMessage,
isSelectedFile: false
}
isSelectedFile: false,
},
};
export const DifferentVersions: Story = {
@ -136,10 +136,10 @@ export const DifferentVersions: Story = {
responseMessage: {
...mockResponseMessage,
version_number: 3,
file_name: 'Marketing Dashboard v3'
file_name: 'Marketing Dashboard v3',
},
isSelectedFile: false
}
isSelectedFile: false,
},
};
export const WithMetadata: Story = {
@ -151,15 +151,15 @@ export const WithMetadata: Story = {
{
status: 'loading',
message: 'Loading dashboard data...',
timestamp: Date.now() - 3000
timestamp: Date.now() - 3000,
},
{
status: 'completed',
message: 'Dashboard loaded successfully',
timestamp: Date.now()
}
]
timestamp: Date.now(),
},
],
},
isSelectedFile: false
}
isSelectedFile: false,
},
};

View File

@ -10,7 +10,7 @@ import type { ChatResponseMessageProps } from './ChatResponseMessageSelector';
export const ChatResponseMessage_Text: React.FC<ChatResponseMessageProps> = React.memo(
({ responseMessageId, messageId, isStreamFinished }) => {
const { data: responseMessage } = useGetChatMessage(messageId, {
select: (x) => x?.response_messages?.[responseMessageId]
select: (x) => x?.response_messages?.[responseMessageId],
});
const { message } = responseMessage as BusterChatResponseMessage_text;

View File

@ -1,189 +0,0 @@
import { render, screen } from '@testing-library/react';
import type React from 'react';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { useQuery } from '@tanstack/react-query';
import { useGetChatMessage } from '@/api/buster_rest/chats';
import { useChatLayoutContextSelector } from '../../../ChatLayoutContext';
import { ChatResponseReasoning } from './ChatResponseReasoning';
// Mock the imports
vi.mock('../../../ChatLayoutContext', () => ({
useChatLayoutContextSelector: vi.fn()
}));
vi.mock('@/api/buster_rest/chats', () => ({
useGetChatMessage: vi.fn()
}));
vi.mock('@tanstack/react-query', () => ({
useQuery: vi.fn()
}));
// Mock query keys
vi.mock('@/api/query_keys', () => ({
queryKeys: {
chatsBlackBoxMessages: vi.fn().mockReturnValue({
queryKey: ['chats', 'blackBoxMessages'],
notifyOnChangeProps: ['data']
})
}
}));
// Mock next/link
vi.mock('next/link', () => ({
default: ({ children, href }: { children: React.ReactNode; href: string }) => (
<a href={href} data-testid="link">
{children}
</a>
)
}));
// Mock routes
vi.mock('@/routes', () => ({
BusterRoutes: {
APP_CHAT_ID: '/app/chat/:chatId',
APP_CHAT_ID_REASONING_ID: '/app/chat/:chatId/:messageId/reasoning'
},
createBusterRoute: vi.fn().mockImplementation(({ route, chatId, messageId }) => {
if (route === '/app/chat/:chatId') {
return `/app/chat/${chatId}`;
}
if (route === '/app/chat/:chatId/:messageId/reasoning') {
return `/app/chat/${chatId}/${messageId}/reasoning`;
}
return '';
})
}));
// Mock ShimmerText component
vi.mock('@/components/ui/typography/ShimmerText', () => ({
ShimmerText: ({ text }: { text: string }) => <span data-testid="shimmer">{text}</span>
}));
// Mock Text component
vi.mock('@/components/ui/typography', () => ({
Text: ({
children,
variant,
className
}: {
children: React.ReactNode;
variant?: string;
className?: string;
}) => (
<span data-testid="text" data-variant={variant} className={className}>
{children}
</span>
)
}));
// Mock framer-motion components
vi.mock('framer-motion', () => ({
motion: {
div: ({ children, ...props }: any) => <div {...props}>{children}</div>
},
AnimatePresence: ({ children }: any) => <>{children}</>
}));
describe('ChatResponseReasoning', () => {
const defaultProps = {
reasoningMessageId: 'reasoning-id',
finalReasoningMessage: undefined,
isStreamFinished: false,
messageId: 'message-id',
chatId: 'chat-id'
};
beforeEach(() => {
vi.clearAllMocks();
// Default mock implementation
(useChatLayoutContextSelector as any).mockImplementation((selector: any) => {
if (selector.toString().includes('messageId')) return 'different-message-id';
if (selector.toString().includes('selectedFileType')) return 'not-reasoning';
return null;
});
// Mock useGetChatMessage with proper selector behavior
(useGetChatMessage as any).mockImplementation((id: any, options: any) => {
if (options?.select?.toString().includes('reasoning_messages')) {
return { data: 'Test Title' };
}
if (options?.select?.toString().includes('final_reasoning_message')) {
return { data: null };
}
return { data: null };
});
(useQuery as any).mockReturnValue({
data: null
});
});
it('renders with default state showing ShimmerText', () => {
render(<ChatResponseReasoning {...defaultProps} />);
// We should have a link
const link = screen.getByTestId('link');
expect(link).toBeInTheDocument();
// Since isCompletedStream is false and we're not in reasoning view, ShimmerText should be shown
expect(screen.getByTestId('shimmer')).toBeInTheDocument();
expect(screen.getByText('Test Title')).toBeInTheDocument();
});
it('displays finalReasoningMessage when available', () => {
(useGetChatMessage as any).mockImplementation((id: any, options: any) => {
if (options?.select?.toString().includes('reasoning_messages')) {
return { data: 'Test Title' };
}
if (options?.select?.toString().includes('final_reasoning_message')) {
return { data: 'Final reasoning message' };
}
return { data: null };
});
render(
<ChatResponseReasoning
{...defaultProps}
finalReasoningMessage="Final reasoning message"
isStreamFinished={true}
/>
);
expect(screen.getByText('Final reasoning message')).toBeInTheDocument();
});
it('displays blackBoxMessage when available', () => {
(useQuery as any).mockReturnValue({
data: 'Black box message'
});
render(<ChatResponseReasoning {...defaultProps} isStreamFinished={true} />);
expect(screen.getByText('Black box message')).toBeInTheDocument();
});
it('renders with correct link when reasoning file is selected', () => {
(useChatLayoutContextSelector as any).mockImplementation((selector: any) => {
if (selector.toString().includes('messageId')) return 'message-id';
if (selector.toString().includes('selectedFileType')) return 'reasoning';
return null;
});
render(<ChatResponseReasoning {...defaultProps} isStreamFinished={true} />);
// When reasoning file is selected, link should point to chat without reasoning
const link = screen.getByTestId('link');
expect(link).toHaveAttribute('href', '/app/chat/chat-id');
});
it('renders "Thinking..." as fallback text', () => {
(useGetChatMessage as any).mockImplementation(() => ({ data: null }));
(useQuery as any).mockReturnValue({ data: null });
render(<ChatResponseReasoning {...defaultProps} />);
expect(screen.getByText('Getting started...')).toBeInTheDocument();
});
});

View File

@ -15,14 +15,16 @@ export const ChatScrollToBottom: React.FC<{
isAutoScrollEnabled
? 'pointer-events-none scale-90 opacity-0'
: 'pointer-events-auto scale-100 cursor-pointer opacity-100'
)}>
)}
>
<AppTooltip title="Stick to bottom" sideOffset={12} delayDuration={500}>
<button
type="button"
onClick={scrollToBottom}
className={
'bg-background/90 hover:bg-item-hover/90 cursor-pointer rounded-full border p-2 shadow transition-all duration-300 hover:shadow-md'
}>
}
>
<ChevronDown />
</button>
</AppTooltip>

View File

@ -12,6 +12,7 @@ import { useBusterNotifications } from '@/context/BusterNotifications';
import { useMemoizedFn } from '@/hooks/useMemoizedFn';
import { useMount } from '@/hooks/useMount';
import { cn } from '@/lib/classMerge';
import { useChat } from '../../../context/Chats/useChat';
import { MessageContainer } from './MessageContainer';
export const ChatUserMessage: React.FC<{
@ -131,7 +132,7 @@ const EditMessage: React.FC<{
}> = React.memo(({ requestMessage, onSetIsEditing, messageId, chatId }) => {
const [prompt, setPrompt] = useState(requestMessage.request || '');
const textAreaRef = useRef<HTMLTextAreaElement>(null);
const onReplaceMessageInChat = useBusterNewChatContextSelector((x) => x.onReplaceMessageInChat);
const { onReplaceMessageInChat } = useChat();
const onSave = useMemoizedFn(() => {
onReplaceMessageInChat({

View File

@ -1,8 +1,8 @@
import type React from 'react';
import { forwardRef } from 'react';
import { Avatar } from '@/components/ui/avatar';
import { cn } from '@/lib/classMerge';
import { BusterLoadingAvatar } from '@/components/ui/avatar/BusterLoadingAvatar';
import { cn } from '@/lib/classMerge';
interface MessageContainerProps {
children: React.ReactNode;
@ -23,7 +23,6 @@ export const MessageContainer = forwardRef<HTMLDivElement, MessageContainerProps
{
children,
senderName,
senderId,
senderAvatar,
className = '',
hideAvatar = false,
@ -31,7 +30,7 @@ export const MessageContainer = forwardRef<HTMLDivElement, MessageContainerProps
isStreamFinished = true,
isFinishedReasoning = true,
onMouseEnter,
onMouseLeave
onMouseLeave,
},
ref
) => {
@ -40,7 +39,8 @@ export const MessageContainer = forwardRef<HTMLDivElement, MessageContainerProps
ref={ref}
className={'flex w-full space-x-2'}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}>
onMouseLeave={onMouseLeave}
>
<div className={cn('w-6 transition-opacity', hideAvatar ? 'opacity-0' : 'opacity-100')}>
{senderName ? (
<Avatar size={24} name={senderName} image={senderAvatar || ''} useToolTip={true} />

View File

@ -55,12 +55,19 @@ type CollectionParamsToRoute = {
dashboardVersionNumber?: number;
};
type ReasoningParamsToRoute = {
assetType: 'reasoning';
assetId: string;
chatId: string;
};
export type AssetParamsToRoute =
| ChatParamsToRoute
| MetricParamsToRoute
| DashboardParamsToRoute
| ReportParamsToRoute
| CollectionParamsToRoute;
| CollectionParamsToRoute
| ReasoningParamsToRoute;
/**
* Route builder internal state type

View File

@ -29,10 +29,10 @@ function RouteComponent() {
declare module '@tanstack/react-router' {
interface StaticDataRouteOption {
assetType?: AssetType;
assetType?: AssetType | 'reasoning';
}
interface RouteContext {
assetType?: AssetType;
assetType?: AssetType | 'reasoning';
}
}

View File

@ -2,6 +2,9 @@ import { createFileRoute } from '@tanstack/react-router';
export const Route = createFileRoute('/app/_app/_asset/chats/$chatId/reasoning/$messageId')({
component: RouteComponent,
staticData: {
assetType: 'reasoning',
},
});
function RouteComponent() {

View File

@ -17,7 +17,7 @@ const ResponseMessage_FileMetadataSchema = z.object({
timestamp: z.number().optional(),
});
const ResponseMessageFileTypeSchema = z.enum(['metric', 'dashboard', 'report']);
const ResponseMessageFileTypeSchema = z.enum(['metric', 'dashboard', 'report', 'reasoning']);
const ResponseMessage_FileSchema = z.object({
id: z.string(),