fix: chat loading-state model placeholder (#8431)
Signed-off-by: sunilkumarvalmiki <g.sunilkumarvalmiki@gmail.com> Co-authored-by: jh-block <jhugo@block.xyz>
This commit is contained in:
@@ -0,0 +1,109 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { render, type RenderOptions, screen } from '@testing-library/react';
|
||||
import ModelsBottomBar from './ModelsBottomBar';
|
||||
import { IntlTestWrapper } from '../../../../i18n/test-utils';
|
||||
|
||||
const renderWithIntl = (ui: React.ReactElement, options?: RenderOptions) =>
|
||||
render(ui, { wrapper: IntlTestWrapper, ...options });
|
||||
|
||||
const createDropdownRef = (): React.RefObject<HTMLDivElement> =>
|
||||
({ current: document.createElement('div') }) as React.RefObject<HTMLDivElement>;
|
||||
|
||||
let mockCurrentModel: string | null = 'config-model';
|
||||
let mockCurrentProvider: string | null = 'config-provider';
|
||||
const mockGetProviders = vi.fn();
|
||||
const mockOnModelChanged = vi.fn();
|
||||
|
||||
vi.mock('../../../ModelAndProviderContext', () => ({
|
||||
useModelAndProvider: () => ({
|
||||
currentModel: mockCurrentModel,
|
||||
currentProvider: mockCurrentProvider,
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock('../../../ConfigContext', () => ({
|
||||
useConfig: () => ({
|
||||
getProviders: mockGetProviders,
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock('../modelInterface', () => ({
|
||||
getProviderMetadata: vi.fn().mockResolvedValue({ display_name: 'Config Provider' }),
|
||||
}));
|
||||
|
||||
vi.mock('../predefinedModelsUtils', () => ({
|
||||
getModelDisplayName: (model: string) => `Display ${model}`,
|
||||
}));
|
||||
|
||||
vi.mock('../../../bottom_menu/BottomMenuAlertPopover', () => ({
|
||||
default: () => null,
|
||||
}));
|
||||
|
||||
vi.mock('../../../ui/dropdown-menu', () => ({
|
||||
DropdownMenu: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
|
||||
DropdownMenuTrigger: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
|
||||
DropdownMenuContent: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
|
||||
DropdownMenuItem: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
|
||||
}));
|
||||
|
||||
vi.mock('../../localInference/ModelSettingsPanel', () => ({
|
||||
ModelSettingsPanel: () => null,
|
||||
}));
|
||||
|
||||
vi.mock('../../../ui/scroll-area', () => ({
|
||||
ScrollArea: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
|
||||
}));
|
||||
|
||||
describe('ModelsBottomBar', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
mockCurrentModel = 'config-model';
|
||||
mockCurrentProvider = 'config-provider';
|
||||
mockGetProviders.mockResolvedValue([]);
|
||||
});
|
||||
|
||||
it('shows a loading placeholder while the active session model is still loading', async () => {
|
||||
renderWithIntl(
|
||||
<ModelsBottomBar
|
||||
sessionId="session-123"
|
||||
dropdownRef={createDropdownRef()}
|
||||
setView={vi.fn()}
|
||||
onModelChanged={mockOnModelChanged}
|
||||
sessionLoaded={false}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByTestId('model-loading-state')).toHaveTextContent('Loading model...');
|
||||
});
|
||||
|
||||
it('shows the active session model once the session has loaded', async () => {
|
||||
renderWithIntl(
|
||||
<ModelsBottomBar
|
||||
sessionId="session-123"
|
||||
dropdownRef={createDropdownRef()}
|
||||
setView={vi.fn()}
|
||||
sessionModel="session-model"
|
||||
sessionProvider="session-provider"
|
||||
onModelChanged={mockOnModelChanged}
|
||||
sessionLoaded={true}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByText('session-model')).toBeInTheDocument();
|
||||
expect(screen.queryByTestId('model-loading-state')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows the configured model when there is no active session', async () => {
|
||||
renderWithIntl(
|
||||
<ModelsBottomBar
|
||||
sessionId={null}
|
||||
dropdownRef={createDropdownRef()}
|
||||
setView={vi.fn()}
|
||||
onModelChanged={mockOnModelChanged}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByText('config-model')).toBeInTheDocument();
|
||||
expect(screen.queryByTestId('model-loading-state')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Sliders, Bot, Settings } from 'lucide-react';
|
||||
import { Sliders, Bot, LoaderCircle, Settings } from 'lucide-react';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useModelAndProvider } from '../../../ModelAndProviderContext';
|
||||
import { SwitchModelModal } from '../subcomponents/SwitchModelModal';
|
||||
@@ -26,6 +26,10 @@ const i18n = defineMessages({
|
||||
id: 'modelsBottomBar.currentModel',
|
||||
defaultMessage: 'Current model',
|
||||
},
|
||||
loadingModel: {
|
||||
id: 'modelsBottomBar.loadingModel',
|
||||
defaultMessage: 'Loading model...',
|
||||
},
|
||||
changeModel: {
|
||||
id: 'modelsBottomBar.changeModel',
|
||||
defaultMessage: 'Change Model',
|
||||
@@ -73,10 +77,13 @@ export default function ModelsBottomBar({
|
||||
const [isLocalModelSettingsOpen, setIsLocalModelSettingsOpen] = useState(false);
|
||||
const [providerDefaultModel, setProviderDefaultModel] = useState<string | null>(null);
|
||||
|
||||
// Hide label while session data is still being fetched (avoids flashing
|
||||
// the config default before the session's actual model arrives).
|
||||
const isModelLoading = sessionId && !sessionLoaded;
|
||||
// Show a visible loading placeholder while session metadata is still being fetched,
|
||||
// rather than flashing the config default or leaving the footer blank.
|
||||
const isModelLoading = Boolean(sessionId && !sessionLoaded);
|
||||
const displayModel = currentModel || providerDefaultModel || displayModelName;
|
||||
const loadingModelLabel = intl.formatMessage(i18n.loadingModel);
|
||||
const triggerLabel = isModelLoading ? loadingModelLabel : displayModel;
|
||||
const menuModelLabel = isModelLoading ? loadingModelLabel : displayModelName;
|
||||
|
||||
useEffect(() => {
|
||||
if (!currentProvider) return;
|
||||
@@ -121,16 +128,24 @@ export default function ModelsBottomBar({
|
||||
<DropdownMenuTrigger className="flex items-center hover:cursor-pointer max-w-[180px] md:max-w-[200px] lg:max-w-[380px] min-w-0 text-text-primary/70 hover:text-text-primary transition-colors">
|
||||
<div className="flex items-center truncate max-w-[130px] md:max-w-[200px] lg:max-w-[360px] min-w-0">
|
||||
<Bot className="mr-1 h-4 w-4 flex-shrink-0" />
|
||||
<span className={`truncate text-xs${isModelLoading ? ' opacity-0' : ''}`}>
|
||||
{displayModel}
|
||||
</span>
|
||||
{isModelLoading ? (
|
||||
<span
|
||||
data-testid="model-loading-state"
|
||||
className="inline-flex items-center gap-1 truncate text-xs"
|
||||
>
|
||||
<LoaderCircle className="h-3 w-3 animate-spin flex-shrink-0" />
|
||||
<span className="truncate">{triggerLabel}</span>
|
||||
</span>
|
||||
) : (
|
||||
<span className="truncate text-xs">{triggerLabel}</span>
|
||||
)}
|
||||
</div>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent side="top" align="center" className="w-64 text-sm">
|
||||
<h6 className="text-xs text-text-primary mt-2 ml-2">{intl.formatMessage(i18n.currentModel)}</h6>
|
||||
<p className="flex items-center justify-between text-sm mx-2 pb-2 border-b mb-2">
|
||||
{displayModelName}
|
||||
{displayProvider && ` — ${displayProvider}`}
|
||||
{menuModelLabel}
|
||||
{!isModelLoading && displayProvider && ` — ${displayProvider}`}
|
||||
</p>
|
||||
<DropdownMenuItem onClick={() => setIsAddModelModalOpen(true)}>
|
||||
<span>{intl.formatMessage(i18n.changeModel)}</span>
|
||||
|
||||
@@ -2315,6 +2315,9 @@
|
||||
"modelsBottomBar.currentModel": {
|
||||
"defaultMessage": "Current model"
|
||||
},
|
||||
"modelsBottomBar.loadingModel": {
|
||||
"defaultMessage": "Loading model..."
|
||||
},
|
||||
"modelsBottomBar.localModelSettings": {
|
||||
"defaultMessage": "Local Model Settings"
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user