Skip to content

Commit 02f534c

Browse files
committed
feat: new side bar
1 parent 29bf4b8 commit 02f534c

File tree

6 files changed

+118
-139
lines changed

6 files changed

+118
-139
lines changed

src/client/components/ChatV2/ChatBox.tsx

Lines changed: 7 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,12 @@ import StopIcon from '@mui/icons-material/Stop'
44
import AttachFileIcon from '@mui/icons-material/AttachFile'
55
import RestartAltIcon from '@mui/icons-material/RestartAlt'
66
import SettingsIcon from '@mui/icons-material/Settings'
7-
import { Box, Chip, IconButton, TextField, Tooltip, Typography, FormControlLabel, Switch, Alert } from '@mui/material'
7+
import { Box, Chip, IconButton, TextField, Tooltip, Typography, Alert } from '@mui/material'
88
import CircularProgress from '@mui/material/CircularProgress'
99
import { useRef } from 'react'
1010
import useUserStatus from '../../hooks/useUserStatus'
1111
import { useParams } from 'react-router-dom'
1212
import { useTranslation } from 'react-i18next'
13-
import ModelSelector from './ModelSelector'
1413
import { BlueButton, GrayButton, OutlineButtonBlack } from './general/Buttons'
1514
import { useIsEmbedded } from '../../contexts/EmbeddedContext'
1615
import useCurrentUser from '../../hooks/useCurrentUser'
@@ -19,10 +18,8 @@ import { KeyCombinations, useKeyboardCommands } from './useKeyboardCommands'
1918

2019
export const ChatBox = ({
2120
disabled,
22-
currentModel,
2321
fileInputRef,
2422
fileName,
25-
availableModels,
2623
tokenUsageWarning,
2724
tokenUsageAlertOpen,
2825
saveConsent,
@@ -32,18 +29,15 @@ export const ChatBox = ({
3229
saveChat,
3330
notOptoutSaving,
3431
setFileName,
35-
setModel,
3632
handleCancel,
3733
handleContinue,
3834
handleSubmit,
3935
handleReset,
4036
isMobile,
4137
}: {
4238
disabled: boolean
43-
currentModel: string
4439
fileInputRef: React.RefObject<HTMLInputElement>
4540
fileName: string
46-
availableModels: string[]
4741
tokenUsageWarning: string
4842
tokenUsageAlertOpen: boolean
4943
saveConsent: boolean
@@ -53,7 +47,6 @@ export const ChatBox = ({
5347
saveChat: boolean
5448
notOptoutSaving: boolean
5549
setFileName: (name: string) => void
56-
setModel: (model: string) => void
5750
handleCancel: () => void
5851
handleContinue: (message: string) => void
5952
handleSubmit: (message: string) => void
@@ -77,13 +70,12 @@ export const ChatBox = ({
7770

7871
const { t } = useTranslation()
7972

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
73+
// useKeyboardCommands({
74+
// resetChat: handleReset,
75+
// openModelSelector: () => {
76+
// setIsModelSelectorOpen(true)
77+
// },
78+
// }) // @todo what key combination to open model selector
8779

8880
const isShiftEnterSend = user?.preferences?.sendShortcutMode === 'shift+enter' || !user?.preferences?.sendShortcutMode
8981

@@ -245,23 +237,7 @@ export const ChatBox = ({
245237
</IconButton>
246238
</Tooltip>
247239
{fileName && <Chip sx={{ borderRadius: 100 }} label={fileName} onDelete={handleDeleteFile} />}
248-
{!isEmbedded && (
249-
<ModelSelector
250-
currentModel={currentModel}
251-
setModel={setModel}
252-
availableModels={availableModels}
253-
isTokenLimitExceeded={isTokenLimitExceeded}
254-
isOpen={isModelSelectorOpen}
255-
setIsOpen={(open) => {
256-
setIsModelSelectorOpen(open)
257-
if (!open) {
258-
setTimeout(() => textFieldRef.current?.focus(), 0) // setTimeout required here...
259-
}
260-
}}
261-
/>
262-
)}
263240
</Box>
264-
265241
<Tooltip title={disabled ? t('chat:cancelResponse') : isShiftEnterSend ? t('chat:shiftEnterSend') : t('chat:enterSend')} arrow placement="top">
266242
<IconButton type={disabled ? 'button' : 'submit'} ref={sendButtonRef} data-testid="send-chat-message">
267243
{disabled ? <StopIcon /> : <Send />}

src/client/components/ChatV2/ChatV2.tsx

Lines changed: 38 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ import PromptSelector from './PromptSelector'
4040
import { useQuery } from '@tanstack/react-query'
4141
import { useMutation } from '@tanstack/react-query'
4242
import apiClient from '../../util/apiClient'
43+
import ModelSelector from './ModelSelector'
4344

4445
function useLocalStorageStateWithURLDefault(key: string, defaultValue: string, urlKey: string) {
4546
const [value, setValue] = useLocalStorageState(key, defaultValue)
@@ -391,6 +392,9 @@ export const ChatV2 = () => {
391392
messages={messages}
392393
activePrompt={activePrompt}
393394
setActivePrompt={setActivePrompt}
395+
currentModel={activeModel}
396+
setModel={setActiveModel}
397+
availableModels={allowedModels}
394398
/>
395399
</Drawer>
396400
) : (
@@ -412,6 +416,9 @@ export const ChatV2 = () => {
412416
messages={messages}
413417
activePrompt={activePrompt}
414418
setActivePrompt={setActivePrompt}
419+
currentModel={activeModel}
420+
setModel={setActiveModel}
421+
availableModels={allowedModels}
415422
/>
416423
))}
417424

@@ -493,8 +500,6 @@ export const ChatV2 = () => {
493500
>
494501
<ChatBox
495502
disabled={isStreaming}
496-
currentModel={activeModel}
497-
availableModels={allowedModels}
498503
fileInputRef={fileInputRef}
499504
fileName={fileName}
500505
setFileName={setFileName}
@@ -506,7 +511,6 @@ export const ChatV2 = () => {
506511
tokenUsageAlertOpen={tokenUsageAlertOpen}
507512
saveChat={!!course && course.saveDiscussions}
508513
notOptoutSaving={!!course && course.notOptoutSaving}
509-
setModel={(model) => setActiveModel(model)}
510514
handleCancel={handleCancel}
511515
handleContinue={(newMessage) => handleSubmit(newMessage, true)}
512516
handleSubmit={(newMessage) => {
@@ -605,6 +609,10 @@ const LeftMenu = ({
605609
messages,
606610
activePrompt,
607611
setActivePrompt,
612+
currentModel,
613+
setModel,
614+
availableModels,
615+
608616
}: {
609617
sx?: object
610618
course?: Course
@@ -620,13 +628,26 @@ const LeftMenu = ({
620628
messages: Message[]
621629
activePrompt: Prompt | undefined
622630
setActivePrompt: (prompt: Prompt | undefined) => void
631+
632+
currentModel: string
633+
setModel: (model: string) => void
634+
availableModels: string[]
623635
}) => {
636+
const { courseId } = useParams()
637+
const { userStatus, isLoading: statusLoading } = useUserStatus(courseId)
638+
const [isTokenLimitExceeded, setIsTokenLimitExceeded] = useState<boolean>(false)
639+
const [isOpen, setIsOpen] = useState<boolean>(false)
624640
const urlPromptId = useUrlPromptId()
625-
const { data: myPrompts, refetch } = useQuery<Prompt[]>({
641+
const { data: myPrompts } = useQuery<Prompt[]>({
626642
queryKey: ['/prompts/my-prompts'],
627643
initialData: [],
628644
})
629645

646+
useEffect(() => {
647+
if (!userStatus) return
648+
setIsTokenLimitExceeded(userStatus.usage > userStatus.limit)
649+
}, [statusLoading, userStatus])
650+
630651
return (
631652
<Box
632653
sx={[
@@ -648,14 +669,12 @@ const LeftMenu = ({
648669
<OutlineButtonBlack startIcon={<RestartAltIcon />} onClick={handleReset} data-testid="empty-conversation-button">
649670
{t('chat:emptyConversation')}
650671
</OutlineButtonBlack>
651-
652-
<EmailButton messages={messages} disabled={!messages?.length} />
653-
<OutlineButtonBlack startIcon={<Tune />} onClick={() => setSettingsModalOpen(true)} data-testid="settings-button">
654-
{t('chat:settings')}
655-
</OutlineButtonBlack>
656-
<OutlineButtonBlack startIcon={<HelpIcon />} onClick={() => setDisclaimerStatus(true)} data-testid="help-button">
657-
{t('info:title')}
658-
</OutlineButtonBlack>
672+
<ModelSelector
673+
currentModel={currentModel}
674+
setModel={setModel}
675+
availableModels={availableModels}
676+
isTokenLimitExceeded={isTokenLimitExceeded}
677+
/>
659678
<PromptSelector
660679
sx={{ width: '100%' }}
661680
coursePrompts={course?.prompts ?? []}
@@ -665,6 +684,13 @@ const LeftMenu = ({
665684
mandatoryPrompt={course?.prompts.find((p) => p.mandatory)}
666685
urlPrompt={course?.prompts.find((p) => p.id === urlPromptId)}
667686
/>
687+
<EmailButton messages={messages} disabled={!messages?.length} />
688+
<OutlineButtonBlack startIcon={<Tune />} onClick={() => setSettingsModalOpen(true)} data-testid="settings-button">
689+
{t('chat:settings')}
690+
</OutlineButtonBlack>
691+
<OutlineButtonBlack startIcon={<HelpIcon />} onClick={() => setDisclaimerStatus(true)} data-testid="help-button">
692+
{t('info:title')}
693+
</OutlineButtonBlack>
668694
{course && showRagSelector && (
669695
<>
670696
<Typography variant="h6" sx={{ mb: 1, display: 'flex', gap: 1, alignItems: 'center' }} fontWeight="bold">

src/client/components/ChatV2/ModelSelector.tsx

Lines changed: 55 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -1,113 +1,78 @@
1+
import React from 'react'
12
import { useTranslation } from 'react-i18next'
2-
import { MenuItem, FormControl, Select, SelectChangeEvent, Typography, Tooltip } from '@mui/material'
3+
import { MenuItem, Typography, Tooltip, Menu } from '@mui/material'
4+
import { KeyboardArrowDown, SmartToy } from '@mui/icons-material'
35
import { FREE_MODEL } from '../../../config'
4-
import { KeyCombinations } from './useKeyboardCommands'
5-
import React from 'react'
6+
import { OutlineButtonBlack } from './general/Buttons'
67

78
const ModelSelector = ({
89
currentModel,
910
setModel,
1011
availableModels,
1112
isTokenLimitExceeded,
12-
isOpen,
13-
setIsOpen,
1413
}: {
1514
currentModel: string
1615
setModel: (model: string) => void
1716
availableModels: string[]
1817
isTokenLimitExceeded: boolean
19-
isOpen: boolean
20-
setIsOpen: (isOpen: boolean) => void
2118
}) => {
2219
const { t } = useTranslation()
23-
const selectRef = React.useRef<HTMLSelectElement>(null)
24-
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)
20+
const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null)
21+
const open = Boolean(anchorEl)
2922
const validModel = availableModels.includes(currentModel) ? currentModel : ''
3023

24+
const handleClick = (event: React.MouseEvent<HTMLElement>) => {
25+
setAnchorEl(event.currentTarget)
26+
}
27+
28+
const handleSelect = (model: string) => {
29+
setModel(model)
30+
setAnchorEl(null)
31+
}
32+
3133
return (
32-
<FormControl
33-
sx={{
34-
minWidth: 100,
35-
maxWidth: { xs: 60, md: 250 },
36-
opacity: 0.7,
37-
}}
38-
size="small"
39-
>
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}
34+
<>
35+
<OutlineButtonBlack
36+
startIcon={<SmartToy />}
37+
endIcon={<KeyboardArrowDown />}
38+
onClick={handleClick}
39+
data-testid="model-selector"
5240
>
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-
},
41+
{`${t('admin:model')}: ${validModel}`}
42+
</OutlineButtonBlack>
43+
<Menu
44+
anchorEl={anchorEl}
45+
open={open}
46+
onClose={() => setAnchorEl(null)}
47+
slotProps={{
48+
paper: {
49+
style: {
50+
minWidth: anchorEl?.offsetWidth || 200,
8351
},
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>
110-
</FormControl>
52+
},
53+
}}
54+
>
55+
{availableModels.map((model) => (
56+
<MenuItem key={model} value={model} onClick={() => handleSelect(model)} disabled={isTokenLimitExceeded && model !== FREE_MODEL}>
57+
<Typography>
58+
{model}
59+
{model === FREE_MODEL && (
60+
<Typography component="span" sx={{ fontStyle: 'italic', opacity: 0.8 }}>
61+
{' '}
62+
({t('chat:freeModel')})
63+
</Typography>
64+
)}
65+
{isTokenLimitExceeded && model !== FREE_MODEL && (
66+
<Typography component="span" sx={{ fontStyle: 'italic', opacity: 0.8 }}>
67+
{' '}
68+
{t('chat:modelDisabled')}
69+
</Typography>
70+
)}
71+
</Typography>
72+
</MenuItem>
73+
))}
74+
</Menu>
75+
</>
11176
)
11277
}
11378

src/client/components/ChatV2/PromptSelector.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ const PromptSelector = ({
3939
}
4040

4141
return (
42-
<Box sx={{ marginBottom: '0.5rem' }}>
42+
<Box>
4343
<OutlineButtonBlack
4444
sx={sx}
4545
startIcon={<AutoAwesome />}

0 commit comments

Comments
 (0)