Skip to content

Commit 3fdea2b

Browse files
committed
feat: Keyboard shortcut for opening model selector. but only for macos
1 parent 60f3ef5 commit 3fdea2b

File tree

7 files changed

+111
-67
lines changed

7 files changed

+111
-67
lines changed

e2e/chat.spec.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ test.describe('Chat v2 Conversation tests', () => {
99
})
1010

1111
test('Chat v2 mock response works', async ({ page }) => {
12-
await page.locator('#model-selector').first().click()
12+
await page.getByTestId('model-selector').first().click()
1313
await page.getByRole('option', { name: 'mock' }).click()
1414

1515
const chatInput = page.locator('#chat-input').first()
@@ -24,7 +24,7 @@ test.describe('Chat v2 Conversation tests', () => {
2424
})
2525

2626
test('Can empty conversation', async ({ page }) => {
27-
await page.locator('#model-selector').first().click()
27+
await page.getByTestId('model-selector').first().click()
2828
await page.getByRole('option', { name: 'mock' }).click()
2929

3030
const chatInput = page.locator('#chat-input').first()

e2e/courseChatRag.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ test.describe('Course Chat v2', () => {
99
})
1010

1111
test('Course chat works', async ({ page }) => {
12-
await page.locator('#model-selector').first().click()
12+
await page.getByTestId('model-selector').first().click()
1313
await page.getByRole('option', { name: 'mock' }).click()
1414

1515
const chatInput = page.locator('#chat-input').first()

src/client/components/ChatV2/ChatBox.tsx

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import { BlueButton, GrayButton, OutlineButtonBlack } from './general/Buttons'
1515
import { useIsEmbedded } from '../../contexts/EmbeddedContext'
1616
import useCurrentUser from '../../hooks/useCurrentUser'
1717
import { SendPreferenceConfiguratorModal, ShiftEnterForNewline, ShiftEnterToSend } from './SendPreferenceConfigurator'
18-
import { useKeyboardCommands } from './keyboardCommands'
18+
import { KeyCombinations, useKeyboardCommands } from './useKeyboardCommands'
1919

2020
export const ChatBox = ({
2121
disabled,
@@ -70,15 +70,20 @@ export const ChatBox = ({
7070
const [fileTypeAlertOpen, setFileTypeAlertOpen] = useState<boolean>(false)
7171
const [sendPreferenceConfiguratorOpen, setSendPreferenceConfiguratorOpen] = useState<boolean>(false)
7272
const sendButtonRef = useRef<HTMLButtonElement>(null)
73-
7473
const textFieldRef = useRef<HTMLInputElement>(null)
7574
const [message, setMessage] = useState<string>('')
7675

7776
const acuallyDisabled = disabled || message.length === 0
7877

7978
const { t } = useTranslation()
8079

81-
useKeyboardCommands({ resetChat: handleReset })
80+
const [isModelSelectorOpen, setIsModelSelectorOpen] = useState<boolean>(false)
81+
useKeyboardCommands({
82+
resetChat: handleReset,
83+
openModelSelector: () => {
84+
setIsModelSelectorOpen(true)
85+
},
86+
}) // @todo what key combination to open model selector
8287

8388
const isShiftEnterSend = user?.preferences?.sendShortcutMode === 'shift+enter'
8489

@@ -173,7 +178,7 @@ export const ChatBox = ({
173178
onSubmit={onSubmit}
174179
onKeyDown={(e) => {
175180
if (e.key === 'Enter') {
176-
if (user?.preferences?.sendShortcutMode === 'enter') {
181+
if (!isShiftEnterSend) {
177182
if (e.shiftKey) {
178183
// Do nothing with this event, it will result in a newline being inserted
179184
} else {
@@ -232,14 +237,26 @@ export const ChatBox = ({
232237
/>
233238
</IconButton>
234239
</Tooltip>
235-
<Tooltip title={t('chat:emptyConversationTooltip')} arrow placement="top">
240+
<Tooltip title={t('chat:emptyConversationTooltip', { hint: KeyCombinations.RESET_CHAT?.hint })} arrow placement="top">
236241
<IconButton onClick={handleReset}>
237242
<RestartAltIcon />
238243
</IconButton>
239244
</Tooltip>
240245
{fileName && <Chip sx={{ borderRadius: 100 }} label={fileName} onDelete={handleDeleteFile} />}
241246
{!isEmbedded && (
242-
<ModelSelector currentModel={currentModel} setModel={setModel} availableModels={availableModels} isTokenLimitExceeded={isTokenLimitExceeded} />
247+
<ModelSelector
248+
currentModel={currentModel}
249+
setModel={setModel}
250+
availableModels={availableModels}
251+
isTokenLimitExceeded={isTokenLimitExceeded}
252+
isOpen={isModelSelectorOpen}
253+
setIsOpen={(open) => {
254+
setIsModelSelectorOpen(open)
255+
if (!open) {
256+
setTimeout(() => textFieldRef.current?.focus(), 0) // setTimeout required here...
257+
}
258+
}}
259+
/>
243260
)}
244261
</Box>
245262

src/client/components/ChatV2/ModelSelector.tsx

Lines changed: 81 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,31 @@
11
import { useTranslation } from 'react-i18next'
2-
import { MenuItem, FormControl, Select, SelectChangeEvent, Typography } from '@mui/material'
2+
import { MenuItem, FormControl, Select, SelectChangeEvent, Typography, Tooltip } from '@mui/material'
33
import { FREE_MODEL } from '../../../config'
4+
import { KeyCombinations } from './useKeyboardCommands'
5+
import React from 'react'
46

57
const ModelSelector = ({
68
currentModel,
79
setModel,
810
availableModels,
911
isTokenLimitExceeded,
12+
isOpen,
13+
setIsOpen,
1014
}: {
1115
currentModel: string
1216
setModel: (model: string) => void
1317
availableModels: string[]
1418
isTokenLimitExceeded: boolean
19+
isOpen: boolean
20+
setIsOpen: (isOpen: boolean) => void
1521
}) => {
1622
const { t } = useTranslation()
23+
const selectRef = React.useRef<HTMLSelectElement>(null)
1724

25+
/**
26+
* Extra tooltip logic because kb shortcut focusing would leave the tooltip annoyingly open without this.
27+
*/
28+
const [tooltipOpen, setTooltipOpen] = React.useState(false)
1829
const validModel = availableModels.includes(currentModel) ? currentModel : ''
1930

2031
return (
@@ -26,43 +37,76 @@ const ModelSelector = ({
2637
}}
2738
size="small"
2839
>
29-
<Select
30-
sx={{
31-
'& .MuiOutlinedInput-notchedOutline': {
32-
border: 'none',
33-
},
34-
'& .MuiSelect-select': {
35-
overflow: 'hidden',
36-
whiteSpace: 'nowrap',
37-
paddingLeft: { xs: '4px !important', md: '10px !important' },
38-
paddingRight: { xs: '20px !important', md: '30px !important' },
39-
maxWidth: { xs: '50px', md: '240px' },
40-
},
41-
}}
42-
id="model-selector"
43-
value={validModel}
44-
onChange={(event: SelectChangeEvent) => setModel(event.target.value)}
40+
<Tooltip
41+
title={t('chat:modelSelectorTooltip', { hint: KeyCombinations.OPEN_MODEL_SELECTOR?.hint })}
42+
arrow
43+
placement="top"
44+
disableFocusListener
45+
disableHoverListener
46+
onPointerEnter={() => setTooltipOpen(true)}
47+
onFocus={() => setTooltipOpen(true)}
48+
onBlur={() => setTooltipOpen(false)}
49+
onPointerLeave={() => setTooltipOpen(false)}
50+
ref={selectRef}
51+
open={tooltipOpen}
4552
>
46-
{availableModels.map((model) => (
47-
<MenuItem key={model} value={model} disabled={isTokenLimitExceeded && model !== FREE_MODEL}>
48-
<Typography>
49-
{model}
50-
{model === FREE_MODEL && (
51-
<Typography component="span" sx={{ fontStyle: 'italic', opacity: 0.8 }}>
52-
{' '}
53-
({t('chat:freeModel')})
54-
</Typography>
55-
)}
56-
{isTokenLimitExceeded && model !== FREE_MODEL && (
57-
<Typography component="span" sx={{ fontStyle: 'italic', opacity: 0.8 }}>
58-
{' '}
59-
({t('chat:modelDisabled')})
60-
</Typography>
61-
)}
62-
</Typography>
63-
</MenuItem>
64-
))}
65-
</Select>
53+
<Select
54+
ref={selectRef}
55+
open={isOpen}
56+
onOpen={() => {
57+
setIsOpen(true)
58+
setTooltipOpen(true)
59+
}}
60+
onClose={(event) => {
61+
event.stopPropagation()
62+
setTooltipOpen(false)
63+
selectRef.current?.blur()
64+
setIsOpen(false)
65+
}}
66+
sx={{
67+
'& .MuiOutlinedInput-notchedOutline': {
68+
border: 'none',
69+
},
70+
'& .MuiSelect-select': {
71+
overflow: 'hidden',
72+
whiteSpace: 'nowrap',
73+
paddingLeft: { xs: '4px !important', md: '10px !important' },
74+
paddingRight: { xs: '20px !important', md: '30px !important' },
75+
maxWidth: { xs: '50px', md: '240px' },
76+
transition: 'background-color 0.3s ease',
77+
'&:hover': {
78+
backgroundColor: 'action.hover',
79+
},
80+
'&:focus': {
81+
backgroundColor: 'action.focus',
82+
},
83+
},
84+
}}
85+
data-testid="model-selector"
86+
value={validModel}
87+
onChange={(event: SelectChangeEvent) => setModel(event.target.value)}
88+
>
89+
{availableModels.map((model) => (
90+
<MenuItem key={model} value={model} disabled={isTokenLimitExceeded && model !== FREE_MODEL}>
91+
<Typography>
92+
{model}
93+
{model === FREE_MODEL && (
94+
<Typography component="span" sx={{ fontStyle: 'italic', opacity: 0.8 }}>
95+
{' '}
96+
({t('chat:freeModel')})
97+
</Typography>
98+
)}
99+
{isTokenLimitExceeded && model !== FREE_MODEL && (
100+
<Typography component="span" sx={{ fontStyle: 'italic', opacity: 0.8 }}>
101+
{' '}
102+
({t('chat:modelDisabled')})
103+
</Typography>
104+
)}
105+
</Typography>
106+
</MenuItem>
107+
))}
108+
</Select>
109+
</Tooltip>
66110
</FormControl>
67111
)
68112
}

src/client/components/ChatV2/keyboardCommands.ts

Lines changed: 0 additions & 17 deletions
This file was deleted.

src/client/locales/en.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -84,12 +84,12 @@
8484
"denySave": "The conversation may not be saved.",
8585
"toBeSaved": "The discussion will be saved anonymously",
8686
"emptyConversation": "Clear conversation",
87-
"emptyConversationTooltip": "Clear conversation (ctrl + W)",
87+
"emptyConversationTooltip": "Clear conversation {{hint}}",
8888
"emptyConfirm": "Are you sure you want to empty this conversation?",
8989
"settings": "Chat settings",
9090
"shiftEnterSend": "Send (Shift + Enter)",
9191
"enterSend": "Send (Enter)",
92-
"chooseModelTooltip": "Choose model (ctrl + M)",
92+
"modelSelectorTooltip": "Choose model {{hint}}",
9393
"testUseInfo": "The new chat view is still in development and bugs are likely to occur",
9494
"searchTerms": "Searched for:",
9595
"readMore": "Read more",

src/client/locales/fi.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -84,12 +84,12 @@
8484
"denySave": "Keskustelua ei saa tallentaa",
8585
"toBeSaved": "Keskustelu tallennetaan anonyymisti",
8686
"emptyConversation": "Tyhjennä keskustelu",
87-
"emptyConversationTooltip": "Tyhjennä keskustelu (ctrl + W)",
87+
"emptyConversationTooltip": "Tyhjennä keskustelu {{hint}}",
8888
"emptyConfirm": "Oletko varma että tahdot tyhjentää keskustelun?",
8989
"settings": "Keskustelun asetukset",
9090
"shiftEnterSend": "Lähetä (Shift + Enter)",
9191
"enterSend": "Lähetä (Enter)",
92-
"chooseModelTooltip": "Valitse malli (ctrl + M)",
92+
"modelSelectorTooltip": "Valitse malli {{hint}}",
9393
"testUseInfo": "Curren uusi chattinäkymä on vielä kehitysvaiheessa ja bugeja varmasti ilmenee",
9494
"searchTerms": "Hakusanat:",
9595
"readMore": "Lue lisää",

0 commit comments

Comments
 (0)