diff --git a/src/renderer/src/assets/styles/ant.scss b/src/renderer/src/assets/styles/ant.scss index 71e8c58a..57c9cacf 100644 --- a/src/renderer/src/assets/styles/ant.scss +++ b/src/renderer/src/assets/styles/ant.scss @@ -2,10 +2,8 @@ resize: none; } -.chat-nav-dropdown { - .ant-dropdown-menu { - padding-bottom: 12px; - } +.ant-btn:not(:disabled):focus-visible { + outline: none; } .ant-segmented-group { diff --git a/src/renderer/src/components/Popups/SelectModelPopup.tsx b/src/renderer/src/components/Popups/SelectModelPopup.tsx new file mode 100644 index 00000000..47c2506f --- /dev/null +++ b/src/renderer/src/components/Popups/SelectModelPopup.tsx @@ -0,0 +1,184 @@ +import { SearchOutlined } from '@ant-design/icons' +import VisionIcon from '@renderer/components/Icons/VisionIcon' +import { TopView } from '@renderer/components/TopView' +import { getModelLogo, isVisionModel } from '@renderer/config/models' +import { useProviders } from '@renderer/hooks/useProvider' +import { getModelUniqId } from '@renderer/services/model' +import { Model } from '@renderer/types' +import { Avatar, Divider, Empty, Input, InputRef, Menu, MenuProps, Modal } from 'antd' +import { first, reverse, sortBy, upperFirst } from 'lodash' +import { useEffect, useRef, useState } from 'react' +import { useTranslation } from 'react-i18next' +import styled from 'styled-components' + +import { HStack } from '../Layout' + +type MenuItem = Required['items'][number] + +interface Props { + model?: Model +} + +interface PopupContainerProps extends Props { + resolve: (value: Model | undefined) => void +} + +const PopupContainer: React.FC = ({ model, resolve }) => { + const [open, setOpen] = useState(true) + const { t } = useTranslation() + const [searchText, setSearchText] = useState('') + const inputRef = useRef(null) + const { providers } = useProviders() + + const filteredItems: MenuItem[] = providers + .filter((p) => p.models && p.models.length > 0) + .map((p) => ({ + key: p.id, + label: p.isSystem ? t(`provider.${p.id}`) : p.name, + type: 'group', + children: reverse(sortBy(p.models, 'name')) + .filter((m) => m.name.toLowerCase().includes(searchText.toLowerCase())) + .map((m) => ({ + key: getModelUniqId(m), + label: ( + + {upperFirst(m?.name)} {isVisionModel(m) && } + + ), + icon: ( + + {first(m?.name)} + + ), + onClick: () => { + resolve(m) + setOpen(false) + } + })) + })) + .filter((item) => item.children && item.children.length > 0) as MenuItem[] + + const onCancel = () => { + setOpen(false) + } + + const onClose = async () => { + resolve(undefined) + SelectModelPopup.hide() + } + + useEffect(() => { + open && setTimeout(() => inputRef.current?.focus(), 0) + }, [open]) + + return ( + + + + + + } + ref={inputRef} + placeholder={t('model.search')} + value={searchText} + onChange={(e) => setSearchText(e.target.value)} + allowClear + autoFocus + style={{ paddingLeft: 0 }} + bordered={false} + size="middle" + /> + + + + {filteredItems.length > 0 ? ( + + ) : ( + + + + )} + + + ) +} + +const Container = styled.div` + height: 50vh; + margin-top: 10px; + overflow-y: auto; + &::-webkit-scrollbar { + display: none; + } +` + +const StyledMenu = styled(Menu)` + background-color: transparent; + padding: 5px; + margin-top: -10px; + max-height: calc(60vh - 50px); + overflow-y: auto; + + .ant-menu-item-group-title { + padding: 5px 10px 0; + font-size: 12px; + } + + .ant-menu-item { + height: 36px; + line-height: 36px; + } +` + +const ModelItem = styled.div` + display: flex; + align-items: center; + font-size: 14px; +` + +const EmptyState = styled.div` + display: flex; + justify-content: center; + align-items: center; + height: 200px; +` + +const SearchIcon = styled.div` + width: 36px; + height: 36px; + border-radius: 50%; + display: flex; + flex-direction: row; + justify-content: center; + align-items: center; + background-color: var(--color-background-soft); + margin-right: 2px; +` + +export default class SelectModelPopup { + static topviewId = 0 + static hide() { + TopView.hide('SelectModelPopup') + } + static show(params: Props) { + return new Promise((resolve) => { + TopView.show(, 'SelectModelPopup') + }) + } +} diff --git a/src/renderer/src/hooks/useProvider.ts b/src/renderer/src/hooks/useProvider.ts index d25ca54a..dc314eae 100644 --- a/src/renderer/src/hooks/useProvider.ts +++ b/src/renderer/src/hooks/useProvider.ts @@ -22,7 +22,7 @@ export function useProviders() { const dispatch = useAppDispatch() return { - providers, + providers: providers || {}, addProvider: (provider: Provider) => dispatch(addProvider(provider)), removeProvider: (provider: Provider) => dispatch(removeProvider(provider)), updateProvider: (provider: Provider) => dispatch(updateProvider(provider)), diff --git a/src/renderer/src/i18n/en-us.json b/src/renderer/src/i18n/en-us.json index 1370a72c..9446642c 100644 --- a/src/renderer/src/i18n/en-us.json +++ b/src/renderer/src/i18n/en-us.json @@ -115,7 +115,8 @@ "model_settings": "Model Settings" }, "model": { - "stream_output": "Stream Output" + "stream_output": "Stream Output", + "search": "Search models..." }, "files": { "title": "Files", diff --git a/src/renderer/src/i18n/zh-cn.json b/src/renderer/src/i18n/zh-cn.json index cb4ab68c..6cb0a1b4 100644 --- a/src/renderer/src/i18n/zh-cn.json +++ b/src/renderer/src/i18n/zh-cn.json @@ -115,7 +115,8 @@ "model_settings": "模型设置" }, "model": { - "stream_output": "流式输出" + "stream_output": "流式输出", + "search": "搜索模型..." }, "files": { "title": "文件", diff --git a/src/renderer/src/i18n/zh-tw.json b/src/renderer/src/i18n/zh-tw.json index a162d437..fe2871c4 100644 --- a/src/renderer/src/i18n/zh-tw.json +++ b/src/renderer/src/i18n/zh-tw.json @@ -115,7 +115,8 @@ "model_settings": "模型設定" }, "model": { - "stream_output": "串流輸出" + "stream_output": "串流輸出", + "search": "搜尋模型..." }, "files": { "title": "檔案", diff --git a/src/renderer/src/pages/home/Chat.tsx b/src/renderer/src/pages/home/Chat.tsx index f9849e31..b2ef67ad 100644 --- a/src/renderer/src/pages/home/Chat.tsx +++ b/src/renderer/src/pages/home/Chat.tsx @@ -8,7 +8,7 @@ import styled from 'styled-components' import Inputbar from './Inputbar/Inputbar' import Messages from './Messages/Messages' -import RightSidebar from './RightSidebar' +import RightSidebar from './Tabs' interface Props { assistant: Assistant diff --git a/src/renderer/src/pages/home/HomePage.tsx b/src/renderer/src/pages/home/HomePage.tsx index cb7f468e..63cc6c00 100644 --- a/src/renderer/src/pages/home/HomePage.tsx +++ b/src/renderer/src/pages/home/HomePage.tsx @@ -8,7 +8,7 @@ import styled from 'styled-components' import Chat from './Chat' import Navbar from './Navbar' -import RightSidebar from './RightSidebar' +import HomeTabs from './Tabs' let _activeAssistant: Assistant @@ -29,7 +29,7 @@ const HomePage: FC = () => { {showAssistants && ( - = (props) => { [message.content, message.createdAt, onEdit, t] ) + const onSelectModel = async () => { + const selectedModel = await SelectModelPopup.show({ model }) + if (selectedModel) { + setModel(selectedModel) + } + } + return ( {message.role === 'user' && ( @@ -99,13 +105,11 @@ const MessageMenubar: FC = (props) => { {canRegenerate && ( - - - - - - - + + + + + )} {isAssistantMessage && ( diff --git a/src/renderer/src/pages/home/Assistants.tsx b/src/renderer/src/pages/home/Tabs/Assistants.tsx similarity index 100% rename from src/renderer/src/pages/home/Assistants.tsx rename to src/renderer/src/pages/home/Tabs/Assistants.tsx diff --git a/src/renderer/src/pages/home/Settings.tsx b/src/renderer/src/pages/home/Tabs/Settings.tsx similarity index 100% rename from src/renderer/src/pages/home/Settings.tsx rename to src/renderer/src/pages/home/Tabs/Settings.tsx diff --git a/src/renderer/src/pages/home/Topics.tsx b/src/renderer/src/pages/home/Tabs/Topics.tsx similarity index 100% rename from src/renderer/src/pages/home/Topics.tsx rename to src/renderer/src/pages/home/Tabs/Topics.tsx diff --git a/src/renderer/src/pages/home/RightSidebar.tsx b/src/renderer/src/pages/home/Tabs/index.tsx similarity index 97% rename from src/renderer/src/pages/home/RightSidebar.tsx rename to src/renderer/src/pages/home/Tabs/index.tsx index d7b54a8a..598be651 100644 --- a/src/renderer/src/pages/home/RightSidebar.tsx +++ b/src/renderer/src/pages/home/Tabs/index.tsx @@ -27,7 +27,7 @@ type Tab = 'assistants' | 'topic' | 'settings' let _tab: any = '' -const RightSidebar: FC = ({ activeAssistant, activeTopic, setActiveAssistant, setActiveTopic, position }) => { +const HomeTabs: FC = ({ activeAssistant, activeTopic, setActiveAssistant, setActiveTopic, position }) => { const { addAssistant } = useAssistants() const [tab, setTab] = useState(position === 'left' ? _tab || 'assistants' : 'topic') const { topicPosition } = useSettings() @@ -164,4 +164,4 @@ const TabContent = styled.div` overflow-x: hidden; ` -export default RightSidebar +export default HomeTabs diff --git a/src/renderer/src/pages/home/components/SelectModelButton.tsx b/src/renderer/src/pages/home/components/SelectModelButton.tsx index a22335b5..24582ada 100644 --- a/src/renderer/src/pages/home/components/SelectModelButton.tsx +++ b/src/renderer/src/pages/home/components/SelectModelButton.tsx @@ -1,5 +1,6 @@ import ModelAvatar from '@renderer/components/Avatar/ModelAvatar' import VisionIcon from '@renderer/components/Icons/VisionIcon' +import SelectModelPopup from '@renderer/components/Popups/SelectModelPopup' import { isLocalAi } from '@renderer/config/env' import { isVisionModel } from '@renderer/config/models' import { useAssistant } from '@renderer/hooks/useAssistant' @@ -10,8 +11,6 @@ import { FC } from 'react' import { useTranslation } from 'react-i18next' import styled from 'styled-components' -import SelectModelDropdown from './SelectModelDropdown' - interface Props { assistant: Assistant } @@ -24,14 +23,19 @@ const SelectModelButton: FC = ({ assistant }) => { return null } + const onSelectModel = async () => { + const selectedModel = await SelectModelPopup.show({ model }) + if (selectedModel) { + setModel(selectedModel) + } + } + return ( - - - - {model ? upperFirst(model.name) : t('button.select_model')} - {isVisionModel(model) && } - - + + + {model ? upperFirst(model.name) : t('button.select_model')} + {isVisionModel(model) && } + ) } @@ -39,6 +43,7 @@ const DropdownButton = styled(Button)` font-size: 11px; border-radius: 15px; padding: 12px 8px 12px 3px; + -webkit-app-region: none; ` const ModelName = styled.span` diff --git a/src/renderer/src/pages/home/components/SelectModelDropdown.tsx b/src/renderer/src/pages/home/components/SelectModelDropdown.tsx deleted file mode 100644 index 3da63a74..00000000 --- a/src/renderer/src/pages/home/components/SelectModelDropdown.tsx +++ /dev/null @@ -1,64 +0,0 @@ -import VisionIcon from '@renderer/components/Icons/VisionIcon' -import { getModelLogo, isVisionModel } from '@renderer/config/models' -import { useProviders } from '@renderer/hooks/useProvider' -import { getModelUniqId } from '@renderer/services/model' -import { Model } from '@renderer/types' -import { Avatar, Dropdown, DropdownProps, MenuProps } from 'antd' -import { first, reverse, sortBy, upperFirst } from 'lodash' -import { FC, PropsWithChildren } from 'react' -import { useTranslation } from 'react-i18next' -import styled from 'styled-components' - -interface Props extends DropdownProps { - model?: Model - onSelect: (model: Model) => void -} - -const SelectModelDropdown: FC = ({ children, model, onSelect, ...props }) => { - const { t } = useTranslation() - const { providers } = useProviders() - - const items: MenuProps['items'] = (providers || []) - .filter((p) => p.models && p.models.length > 0) - .map((p) => ({ - key: p.id, - label: p.isSystem ? t(`provider.${p.id}`) : p.name, - type: 'group', - children: reverse(sortBy(p.models, 'name')).map((m) => ({ - key: getModelUniqId(m), - label: ( -
- {upperFirst(m?.name)} {isVisionModel(m) && } -
- ), - icon: ( - - {first(m?.name)} - - ), - onClick: () => m && onSelect(m) - })) - })) - - return ( - - {children} - - ) -} - -const DropdownMenu = styled(Dropdown)` - -webkit-app-region: none; -` - -export default SelectModelDropdown