Skip to content

Commit 070be45

Browse files
authored
chore(query-bar): update ai text input styles, move text out of state to store COMPASS-7005 (#4628)
1 parent 88645f7 commit 070be45

File tree

4 files changed

+176
-26
lines changed

4 files changed

+176
-26
lines changed
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import React from 'react';
2+
import type { ComponentProps } from 'react';
3+
import { cleanup, render, screen } from '@testing-library/react';
4+
import { expect } from 'chai';
5+
import sinon from 'sinon';
6+
import type { SinonSpy } from 'sinon';
7+
import { Provider } from 'react-redux';
8+
9+
import { AITextInput } from './ai-text-input';
10+
import { configureStore } from '../../stores/query-bar-store';
11+
import { changeAIPromptText } from '../../stores/ai-query-reducer';
12+
13+
const noop = () => {
14+
/* no op */
15+
};
16+
17+
const renderAITextInput = ({
18+
...props
19+
}: Partial<ComponentProps<typeof AITextInput>> = {}) => {
20+
const store = configureStore();
21+
22+
render(
23+
<Provider store={store}>
24+
<AITextInput onClose={noop} show {...props} />
25+
</Provider>
26+
);
27+
return store;
28+
};
29+
30+
describe('QueryBar Component', function () {
31+
let store: ReturnType<typeof configureStore>;
32+
let onCloseSpy: SinonSpy;
33+
beforeEach(function () {
34+
onCloseSpy = sinon.spy();
35+
});
36+
afterEach(cleanup);
37+
38+
describe('when rendered', function () {
39+
beforeEach(function () {
40+
store = renderAITextInput({
41+
onClose: onCloseSpy,
42+
});
43+
});
44+
45+
it('calls to close robot button is clicked', function () {
46+
expect(onCloseSpy.called).to.be.false;
47+
const closeButton = screen.getByTestId('close-ai-query-button');
48+
expect(closeButton).to.be.visible;
49+
closeButton.click();
50+
expect(onCloseSpy.calledOnce).to.be.true;
51+
});
52+
});
53+
54+
describe('when rendered with text', function () {
55+
beforeEach(function () {
56+
store = renderAITextInput({
57+
onClose: onCloseSpy,
58+
});
59+
store.dispatch(changeAIPromptText('test'));
60+
});
61+
62+
it('calls to clear the text when the X is clicked', function () {
63+
expect(store.getState().aiQuery.aiPromptText).to.equal('test');
64+
65+
const clearTextButton = screen.getByTestId('ai-text-clear-prompt');
66+
expect(clearTextButton).to.be.visible;
67+
clearTextButton.click();
68+
69+
expect(store.getState().aiQuery.aiPromptText).to.equal('');
70+
});
71+
});
72+
});

packages/compass-query-bar/src/components/generative-ai/ai-text-input.tsx

Lines changed: 76 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,20 @@ import {
88
TextInput,
99
css,
1010
cx,
11+
focusRing,
1112
palette,
1213
spacing,
1314
useDarkMode,
1415
} from '@mongodb-js/compass-components';
1516
import { connect } from 'react-redux';
1617

17-
import { RobotSVG } from './robot-svg';
18+
import { DEFAULT_ROBOT_SIZE, RobotSVG } from './robot-svg';
1819
import type { RootState } from '../../stores/query-bar-store';
19-
import { cancelAIQuery, runAIQuery } from '../../stores/ai-query-reducer';
20+
import {
21+
cancelAIQuery,
22+
changeAIPromptText,
23+
runAIQuery,
24+
} from '../../stores/ai-query-reducer';
2025

2126
const containerStyles = css({
2227
display: 'flex',
@@ -95,6 +100,30 @@ const buttonHighlightLightModeStyles = css({
95100
color: palette.gray.dark1,
96101
});
97102

103+
const loaderContainerStyles = css({
104+
padding: spacing[1],
105+
display: 'inline-flex',
106+
width: DEFAULT_ROBOT_SIZE + spacing[2],
107+
justifyContent: 'space-around',
108+
});
109+
110+
const buttonResetStyles = css({
111+
margin: 0,
112+
padding: 0,
113+
border: 'none',
114+
background: 'none',
115+
cursor: 'pointer',
116+
});
117+
118+
const closeAIButtonStyles = css(
119+
buttonResetStyles,
120+
{
121+
padding: spacing[1],
122+
display: 'inline-flex',
123+
},
124+
focusRing
125+
);
126+
98127
const closeText = 'Close AI Query';
99128

100129
const SubmitArrowSVG = ({ darkMode }: { darkMode?: boolean }) => (
@@ -122,25 +151,28 @@ const SubmitArrowSVG = ({ darkMode }: { darkMode?: boolean }) => (
122151
);
123152

124153
type AITextInputProps = {
154+
aiPromptText: string;
125155
onCancelAIQuery: () => void;
126156
isFetching?: boolean;
127157
didSucceed: boolean;
128158
errorMessage?: string;
129159
show: boolean;
160+
onChangeAIPromptText: (text: string) => void;
130161
onClose: () => void;
131162
onSubmitText: (text: string) => void;
132163
};
133164

134165
function AITextInput({
166+
aiPromptText,
135167
onCancelAIQuery,
136168
isFetching,
137169
didSucceed,
138170
errorMessage,
139171
show,
140172
onClose,
173+
onChangeAIPromptText,
141174
onSubmitText,
142175
}: AITextInputProps) {
143-
const [text, setText] = useState('');
144176
const promptTextInputRef = useRef<HTMLInputElement>(null);
145177
const [showSuccess, setShowSuccess] = useState(false);
146178
const darkMode = useDarkMode();
@@ -149,12 +181,12 @@ function AITextInput({
149181
(evt: React.KeyboardEvent<HTMLInputElement>) => {
150182
if (evt.key === 'Enter') {
151183
evt.preventDefault();
152-
onSubmitText(text);
184+
onSubmitText(aiPromptText);
153185
} else if (evt.key === 'Escape') {
154186
isFetching ? onCancelAIQuery() : onClose();
155187
}
156188
},
157-
[text, onClose, onSubmitText, isFetching, onCancelAIQuery]
189+
[aiPromptText, onClose, onSubmitText, isFetching, onCancelAIQuery]
158190
);
159191

160192
useEffect(() => {
@@ -196,23 +228,21 @@ function AITextInput({
196228
sizeVariant="small"
197229
aria-label="Enter a plain text query that the AI will translate into MongoDB query language."
198230
placeholder="Tell Compass what documents to find (e.g. how many users signed up last month)"
199-
value={text}
231+
value={aiPromptText}
200232
onChange={(evt: React.ChangeEvent<HTMLInputElement>) =>
201-
setText(evt.currentTarget.value)
233+
onChangeAIPromptText(evt.currentTarget.value)
202234
}
203235
onKeyDown={onTextInputKeyDown}
204236
/>
205237
<div className={floatingButtonsContainerStyles}>
206-
{isFetching && <SpinLoader />}
207-
{showSuccess && (
208-
<Icon
209-
className={
210-
darkMode
211-
? successIndicatorDarkModeStyles
212-
: successIndicatorLightModeStyles
213-
}
214-
glyph="CheckmarkWithCircle"
215-
/>
238+
{aiPromptText && (
239+
<IconButton
240+
aria-label="Clear query prompt"
241+
onClick={() => onChangeAIPromptText('')}
242+
data-testid="ai-text-clear-prompt"
243+
>
244+
<Icon glyph="X" />
245+
</IconButton>
216246
)}
217247
<Button
218248
size="small"
@@ -221,7 +251,7 @@ function AITextInput({
221251
!darkMode && generateButtonLightModeStyles
222252
)}
223253
onClick={() =>
224-
isFetching ? onCancelAIQuery() : onSubmitText(text)
254+
isFetching ? onCancelAIQuery() : onSubmitText(aiPromptText)
225255
}
226256
>
227257
{isFetching ? (
@@ -245,13 +275,32 @@ function AITextInput({
245275
</>
246276
)}
247277
</Button>
248-
<IconButton
249-
aria-label={closeText}
250-
title={closeText}
251-
onClick={() => onClose()}
252-
>
253-
<RobotSVG />
254-
</IconButton>
278+
{isFetching ? (
279+
<div className={loaderContainerStyles}>
280+
<SpinLoader />
281+
</div>
282+
) : showSuccess ? (
283+
<div className={loaderContainerStyles}>
284+
<Icon
285+
className={
286+
darkMode
287+
? successIndicatorDarkModeStyles
288+
: successIndicatorLightModeStyles
289+
}
290+
glyph="CheckmarkWithCircle"
291+
/>
292+
</div>
293+
) : (
294+
<button
295+
className={closeAIButtonStyles}
296+
data-testid="close-ai-query-button"
297+
aria-label={closeText}
298+
title={closeText}
299+
onClick={() => onClose()}
300+
>
301+
{isFetching ? <SpinLoader /> : <RobotSVG />}
302+
</button>
303+
)}
255304
</div>
256305
</div>
257306
{errorMessage && (
@@ -266,12 +315,14 @@ function AITextInput({
266315
const ConnectedAITextInput = connect(
267316
(state: RootState) => {
268317
return {
318+
aiPromptText: state.aiQuery.aiPromptText,
269319
isFetching: state.aiQuery.status === 'fetching',
270320
didSucceed: state.aiQuery.status === 'success',
271321
errorMessage: state.aiQuery.errorMessage,
272322
};
273323
},
274324
{
325+
onChangeAIPromptText: changeAIPromptText,
275326
onCancelAIQuery: cancelAIQuery,
276327
onSubmitText: runAIQuery,
277328
}

packages/compass-query-bar/src/components/generative-ai/robot-svg.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,12 @@ export const robotSVGLightModeStyles = css({
3030
},
3131
});
3232

33+
export const DEFAULT_ROBOT_SIZE = 20;
34+
3335
// Note: This is duplicated below as a string for HTML.
3436
const RobotSVG = ({
3537
darkMode,
36-
size = 20,
38+
size = DEFAULT_ROBOT_SIZE,
3739
}: {
3840
darkMode?: boolean;
3941
size?: number;

packages/compass-query-bar/src/stores/ai-query-reducer.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,14 @@ type AIQueryStatus = 'ready' | 'fetching' | 'success';
1818
export type AIQueryState = {
1919
errorMessage: string | undefined;
2020
isInputVisible: boolean;
21+
aiPromptText: string;
2122
status: AIQueryStatus;
2223
aiQueryFetchId: number; // Maps to the AbortController of the current fetch (or -1).
2324
};
2425

2526
export const initialState: AIQueryState = {
2627
status: 'ready',
28+
aiPromptText: '',
2729
errorMessage: undefined,
2830
isInputVisible: false,
2931
aiQueryFetchId: -1,
@@ -37,6 +39,7 @@ export const enum AIQueryActionTypes {
3739
CancelAIQuery = 'compass-query-bar/ai-query/CancelAIQuery',
3840
ShowInput = 'compass-query-bar/ai-query/ShowInput',
3941
HideInput = 'compass-query-bar/ai-query/HideInput',
42+
ChangeAIPromptText = 'compass-query-bar/ai-query/ChangeAIPromptText',
4043
}
4144

4245
const NUM_DOCUMENTS_TO_SAMPLE = 4;
@@ -70,6 +73,16 @@ type HideInputAction = {
7073
type: AIQueryActionTypes.HideInput;
7174
};
7275

76+
type ChangeAIPromptTextAction = {
77+
type: AIQueryActionTypes.ChangeAIPromptText;
78+
text: string;
79+
};
80+
81+
export const changeAIPromptText = (text: string): ChangeAIPromptTextAction => ({
82+
type: AIQueryActionTypes.ChangeAIPromptText,
83+
text,
84+
});
85+
7386
type AIQueryStartedAction = {
7487
type: AIQueryActionTypes.AIQueryStarted;
7588
fetchId: number;
@@ -311,6 +324,18 @@ const aiQueryReducer: Reducer<AIQueryState> = (
311324
};
312325
}
313326

327+
if (
328+
isAction<ChangeAIPromptTextAction>(
329+
action,
330+
AIQueryActionTypes.ChangeAIPromptText
331+
)
332+
) {
333+
return {
334+
...state,
335+
aiPromptText: action.text,
336+
};
337+
}
338+
314339
return state;
315340
};
316341

0 commit comments

Comments
 (0)