Skip to content

Commit bd1fbc0

Browse files
Add multiple pane model comparison to playground (opendatahub-io#6169)
* Add multiple pane model comparison to playground * Add unit tests
1 parent 2db1c8a commit bd1fbc0

26 files changed

Lines changed: 2121 additions & 411 deletions

packages/gen-ai/frontend/src/__tests__/cypress/cypress/pages/chatbotPage.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -143,12 +143,13 @@ class ChatbotPage {
143143

144144
// Model Selection
145145
// Model dropdown is now in the toolbar (moved from Model tab)
146+
// Note: There may be multiple model dropdowns (header + settings panel), so use .first()
146147
findModelDropdown(): Cypress.Chainable<JQuery<HTMLElement>> {
147-
return cy.findByTestId('model-selector-toggle');
148+
return cy.findAllByTestId('model-selector-toggle').first();
148149
}
149150

150151
findModelSelectorButton(): Cypress.Chainable<JQuery<HTMLElement>> {
151-
return cy.findByTestId('model-selector-toggle');
152+
return cy.findAllByTestId('model-selector-toggle').first();
152153
}
153154

154155
verifyModelSelected(): void {

packages/gen-ai/frontend/src/__tests__/cypress/cypress/tests/mocked/chatbot/newChatFeature.cy.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,9 @@ describe('Chatbot - New Chat Modal (Mocked)', () => {
8686
cy.wait('@aaModels');
8787

8888
// Verify that a model is selected by checking the dropdown shows a model name
89-
cy.findByRole('button', { name: /Llama 3.2 3B Instruct|Select a model/i })
89+
// Use .first() since there are two model dropdowns (header and settings panel)
90+
cy.findAllByTestId('model-selector-toggle')
91+
.first()
9092
.should('be.visible')
9193
.and('contain', 'Llama');
9294
});

packages/gen-ai/frontend/src/app/Chatbot/ChatbotConfigInstance.tsx

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import {
1818
selectGuardrail,
1919
selectGuardrailUserInputEnabled,
2020
selectGuardrailModelOutputEnabled,
21+
selectRagEnabled,
2122
} from './store';
2223
import { ChatbotMessages } from './ChatbotMessagesList';
2324
import { sampleWelcomePrompts } from './const';
@@ -26,13 +27,13 @@ interface ChatbotConfigInstanceProps {
2627
configId: string;
2728
username?: string;
2829
selectedSourceSettings: ChatbotSourceSettings | null;
29-
isRawUploaded: boolean;
3030
currentVectorStoreId: string | null;
3131
mcpServers: MCPServerFromAPI[];
3232
mcpServerStatuses: Map<string, ServerStatusInfo>;
3333
mcpServerTokens: Map<string, TokenInfo>;
3434
namespace?: string;
3535
showWelcomePrompt?: boolean;
36+
welcomeDescription?: string;
3637
onMessagesHookReady?: (hook: UseChatbotMessagesReturn) => void;
3738
guardrailModelConfigs?: GuardrailModelConfig[];
3839
}
@@ -41,13 +42,13 @@ export const ChatbotConfigInstance: React.FC<ChatbotConfigInstanceProps> = ({
4142
configId,
4243
username,
4344
selectedSourceSettings,
44-
isRawUploaded,
4545
currentVectorStoreId,
4646
mcpServers,
4747
mcpServerStatuses,
4848
mcpServerTokens,
4949
namespace,
5050
showWelcomePrompt = false,
51+
welcomeDescription = 'Welcome to the playground',
5152
onMessagesHookReady,
5253
guardrailModelConfigs = [],
5354
}) => {
@@ -56,6 +57,7 @@ export const ChatbotConfigInstance: React.FC<ChatbotConfigInstanceProps> = ({
5657
const isStreamingEnabled = useChatbotConfigStore(selectStreamingEnabled(configId));
5758
const selectedModel = useChatbotConfigStore(selectSelectedModel(configId));
5859
const selectedMcpServerIds = useChatbotConfigStore(selectSelectedMcpServerIds(configId));
60+
const isRagEnabled = useChatbotConfigStore(selectRagEnabled(configId));
5961

6062
// Guardrails configuration from store
6163
const guardrail = useChatbotConfigStore(selectGuardrail(configId));
@@ -87,7 +89,7 @@ export const ChatbotConfigInstance: React.FC<ChatbotConfigInstanceProps> = ({
8789
modelId: selectedModel,
8890
selectedSourceSettings,
8991
systemInstruction,
90-
isRawUploaded,
92+
isRawUploaded: isRagEnabled,
9193
username,
9294
isStreamingEnabled,
9395
temperature,
@@ -114,7 +116,7 @@ export const ChatbotConfigInstance: React.FC<ChatbotConfigInstanceProps> = ({
114116
{showWelcomePrompt && (
115117
<ChatbotWelcomePrompt
116118
title={username ? `Hello, ${username}` : 'Hello'}
117-
description="Welcome to the playground"
119+
description={welcomeDescription}
118120
data-testid="chatbot-welcome-prompt"
119121
style={{
120122
cursor: 'default',

packages/gen-ai/frontend/src/app/Chatbot/ChatbotHeaderActions.tsx

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import {
1111
MenuToggle,
1212
Tooltip,
1313
} from '@patternfly/react-core';
14-
import { CodeIcon, EllipsisVIcon, PlusIcon } from '@patternfly/react-icons';
14+
import { CodeIcon, ColumnsIcon, EllipsisVIcon, PlusIcon } from '@patternfly/react-icons';
1515
import { ChatbotContext } from '~/app/context/ChatbotContext';
1616
import { useChatbotConfigStore, selectSelectedModel, selectConfigIds } from './store';
1717

@@ -20,13 +20,17 @@ type ChatbotHeaderActionsProps = {
2020
onConfigurePlayground: () => void;
2121
onDeletePlayground: () => void;
2222
onNewChat: () => void;
23+
onCompareChat: () => void;
24+
isCompareMode: boolean;
2325
};
2426

2527
const ChatbotHeaderActions: React.FC<ChatbotHeaderActionsProps> = ({
2628
onViewCode,
2729
onConfigurePlayground,
2830
onDeletePlayground,
2931
onNewChat,
32+
onCompareChat,
33+
isCompareMode,
3034
}) => {
3135
const { lsdStatus, lastInput } = React.useContext(ChatbotContext);
3236
// Might need to iterate through selectedModels for each config during comparison mode
@@ -54,6 +58,20 @@ const ChatbotHeaderActions: React.FC<ChatbotHeaderActionsProps> = ({
5458
<ActionListGroup>
5559
{lsdStatus?.phase === 'Ready' && (
5660
<>
61+
{/* Hide compare button when in compare mode - use close button on pane to exit */}
62+
{!isCompareMode && (
63+
<ActionListItem>
64+
<Button
65+
variant="link"
66+
aria-label="Compare chat"
67+
icon={<ColumnsIcon />}
68+
onClick={onCompareChat}
69+
data-testid="compare-chat-button"
70+
>
71+
Compare chat
72+
</Button>
73+
</ActionListItem>
74+
)}
5775
<ActionListItem>
5876
<Button
5977
variant="link"

packages/gen-ai/frontend/src/app/Chatbot/ChatbotMain.tsx

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,11 @@ import ChatbotEmptyState from '~/app/EmptyStates/NoData';
1212
import { GenAiContext } from '~/app/context/GenAiContext';
1313
import ChatbotConfigurationModal from '~/app/Chatbot/components/chatbotConfiguration/ChatbotConfigurationModal';
1414
import DeletePlaygroundModal from '~/app/Chatbot/components/DeletePlaygroundModal';
15+
import CompareChatModal from '~/app/Chatbot/components/CompareChatModal';
1516
import ChatbotHeader from './ChatbotHeader';
1617
import ChatbotPlayground from './ChatbotPlayground';
1718
import ChatbotHeaderActions from './ChatbotHeaderActions';
19+
import { useChatbotConfigStore, selectConfigIds, DEFAULT_CONFIG_ID } from './store';
1820

1921
const ChatbotMain: React.FunctionComponent = () => {
2022
const {
@@ -37,10 +39,42 @@ const ChatbotMain: React.FunctionComponent = () => {
3739
const [configurationModalOpen, setConfigurationModalOpen] = React.useState(false);
3840
const [deleteModalOpen, setDeleteModalOpen] = React.useState(false);
3941
const [isNewChatModalOpen, setIsNewChatModalOpen] = React.useState(false);
42+
const [isCompareChatModalOpen, setIsCompareChatModalOpen] = React.useState(false);
43+
// Track which pane's settings are active in compare mode
44+
const [activePaneConfigId, setActivePaneConfigId] = React.useState<string>(DEFAULT_CONFIG_ID);
45+
46+
// Ref to clear all chat messages (will be set by ChatbotPlayground)
47+
const clearAllMessagesRef = React.useRef<(() => void) | null>(null);
48+
49+
// Derive compare mode from Zustand store (configIds.length > 1)
50+
const configIds = useChatbotConfigStore(selectConfigIds);
51+
const isCompareMode = configIds.length > 1;
4052

4153
// Check if there are any models available (either AI assets or MaaS models)
4254
const hasModels = aiModels.length > 0 || maasModels.length > 0;
4355

56+
// Handle closing a pane - set active pane to the remaining config
57+
const handleClosePane = React.useCallback((configIdToClose: string) => {
58+
const currentConfigIds = useChatbotConfigStore.getState().configIds;
59+
const remainingConfigId = currentConfigIds.find((id) => id !== configIdToClose);
60+
useChatbotConfigStore.getState().removeConfiguration(configIdToClose);
61+
// Set active pane to the remaining config (or default if somehow none remain)
62+
setActivePaneConfigId(remainingConfigId || DEFAULT_CONFIG_ID);
63+
fireSimpleTrackingEvent('Playground Compare Mode Exited');
64+
}, []);
65+
66+
// Handle compare chat confirmation - clears messages and enters compare mode
67+
const handleCompareConfirm = React.useCallback(() => {
68+
// Clear all chat messages
69+
if (clearAllMessagesRef.current) {
70+
clearAllMessagesRef.current();
71+
}
72+
// Enter compare mode by duplicating the first config
73+
const firstConfigId = useChatbotConfigStore.getState().configIds[0] || DEFAULT_CONFIG_ID;
74+
useChatbotConfigStore.getState().duplicateConfiguration(firstConfigId);
75+
fireSimpleTrackingEvent('Playground Compare Mode Entered');
76+
}, []);
77+
4478
return (
4579
<>
4680
<ApplicationsPage
@@ -108,6 +142,11 @@ const ChatbotMain: React.FunctionComponent = () => {
108142
setIsNewChatModalOpen(true);
109143
fireSimpleTrackingEvent('Playground New Chat Selected');
110144
}}
145+
onCompareChat={() => {
146+
setIsCompareChatModalOpen(true);
147+
fireSimpleTrackingEvent('Playground Compare Chat Selected');
148+
}}
149+
isCompareMode={isCompareMode}
111150
/>
112151
}
113152
>
@@ -117,6 +156,10 @@ const ChatbotMain: React.FunctionComponent = () => {
117156
setIsViewCodeModalOpen={setIsViewCodeModalOpen}
118157
isNewChatModalOpen={isNewChatModalOpen}
119158
setIsNewChatModalOpen={setIsNewChatModalOpen}
159+
activePaneConfigId={activePaneConfigId}
160+
setActivePaneConfigId={setActivePaneConfigId}
161+
onClosePane={handleClosePane}
162+
clearAllMessagesRef={clearAllMessagesRef}
120163
/>
121164
) : lsdStatus?.phase === 'Failed' ? (
122165
<EmptyState
@@ -148,6 +191,11 @@ const ChatbotMain: React.FunctionComponent = () => {
148191
}}
149192
/>
150193
)}
194+
<CompareChatModal
195+
isOpen={isCompareChatModalOpen}
196+
onClose={() => setIsCompareChatModalOpen(false)}
197+
onConfirm={handleCompareConfirm}
198+
/>
151199
</>
152200
);
153201
};
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import * as React from 'react';
2+
import { Card, CardBody } from '@patternfly/react-core';
3+
import ChatbotPaneHeader from './components/ChatbotPaneHeader';
4+
5+
interface ChatbotPaneProps {
6+
/** The configId used for state management */
7+
configId: string;
8+
/** Display label shown in the UI (e.g., "Model 1", "Model 2") */
9+
displayLabel: string;
10+
selectedModel: string;
11+
onModelChange: (model: string) => void;
12+
onSettingsClick: () => void;
13+
onClose: () => void;
14+
children: React.ReactNode;
15+
}
16+
17+
/**
18+
* Wrapper component for a single chatbot pane in compare mode.
19+
* Includes header with label, model dropdown, settings, and close button.
20+
*/
21+
const ChatbotPane: React.FC<ChatbotPaneProps> = ({
22+
configId,
23+
displayLabel,
24+
selectedModel,
25+
onModelChange,
26+
onSettingsClick,
27+
onClose,
28+
children,
29+
}) => (
30+
<Card
31+
isFullHeight
32+
isPlain
33+
style={{ boxShadow: 'none', display: 'flex', flexDirection: 'column', height: '100%' }}
34+
data-testid={`chatbot-pane-${configId}`}
35+
role="region"
36+
aria-label={displayLabel}
37+
>
38+
<ChatbotPaneHeader
39+
label={displayLabel}
40+
selectedModel={selectedModel}
41+
onModelChange={onModelChange}
42+
onSettingsClick={onSettingsClick}
43+
onCloseClick={onClose}
44+
hasDivider
45+
testIdPrefix={`chatbot-pane-${configId}`}
46+
/>
47+
<CardBody
48+
style={{
49+
padding: 0,
50+
overflow: 'hidden',
51+
flex: 1,
52+
display: 'flex',
53+
flexDirection: 'column',
54+
}}
55+
>
56+
{children}
57+
</CardBody>
58+
</Card>
59+
);
60+
61+
export default ChatbotPane;

0 commit comments

Comments
 (0)