From 3aaa1848f00fdd07a0af23285229a6fb5a0f0f6b Mon Sep 17 00:00:00 2001 From: Teo Date: Tue, 8 Apr 2025 19:37:11 +0800 Subject: [PATCH] style(ProviderSettings): Refactor ProviderSettings UI (#4475) * chore(version): 1.1.19 * style(ProviderSettings): Refactor ProviderSettings UI * style(CustomTag, ModelTagsWithLabel): enhance layout and styling for better UI consistency * refactor(CustomTag, ModelTagsWithLabel, MentionModelsButton): update props handling and improve component usage * feat(CustomTag, ModelTagsWithLabel): add tooltip support and improve label visibility based on container size * fix(ModelTagsWithLabel): adjust maxWidth for non-Chinese languages to improve layout * style(ModelList): add text overflow handling for list item names * feat(ModelList): enhance group label with item count using CustomTag * feat(FileItem): add style prop for customizable background color in FileItem component * style(index.scss): update border color variables for improved UI consistency * style(EditModelsPopup): update background color for model items to enhance visual distinction * style(HealthCheckPopup): update button size for improved usability * feat(CustomCollapse): add collapsible prop to customize collapse behavior * chore: remove hover models color --------- Co-authored-by: kangfenmao Co-authored-by: eeee0717 --- src/renderer/src/assets/styles/index.scss | 6 +- .../src/components/CustomCollapse.tsx | 16 +- src/renderer/src/components/CustomTag.tsx | 40 +++ src/renderer/src/components/Icons/SVGIcon.tsx | 13 + .../src/components/ModelTagsWithLabel.tsx | 113 +++++++++ src/renderer/src/pages/files/FileItem.tsx | 33 ++- src/renderer/src/pages/files/FileList.tsx | 4 +- .../home/Inputbar/MentionModelsButton.tsx | 4 +- .../src/pages/home/Tabs/TopicsTab.tsx | 1 - .../src/pages/knowledge/KnowledgeContent.tsx | 92 +++---- .../ProviderSettings/AddModelPopup.tsx | 12 +- .../ProviderSettings/EditModelsPopup.tsx | 230 ++++++++++-------- .../ProviderSettings/HealthCheckPopup.tsx | 2 +- .../ProviderSettings/ModelEditContent.tsx | 14 +- .../settings/ProviderSettings/ModelList.tsx | 208 +++++++++------- .../ProviderSettings/ProviderSetting.tsx | 7 +- src/renderer/src/pages/settings/index.tsx | 2 +- 17 files changed, 518 insertions(+), 279 deletions(-) create mode 100644 src/renderer/src/components/CustomTag.tsx create mode 100644 src/renderer/src/components/Icons/SVGIcon.tsx create mode 100644 src/renderer/src/components/ModelTagsWithLabel.tsx diff --git a/src/renderer/src/assets/styles/index.scss b/src/renderer/src/assets/styles/index.scss index 09659681..13eb0f9a 100644 --- a/src/renderer/src/assets/styles/index.scss +++ b/src/renderer/src/assets/styles/index.scss @@ -36,7 +36,7 @@ --color-text: var(--color-text-1); --color-icon: #ffffff99; --color-icon-white: #ffffff; - --color-border: #ffffff15; + --color-border: #ffffff19; --color-border-soft: #ffffff10; --color-border-mute: #ffffff05; --color-error: #f44336; @@ -80,7 +80,7 @@ body { body[theme-mode='light'] { --color-white: #ffffff; - --color-white-soft: #f2f2f2; + --color-white-soft: rgba(0, 0, 0, 0.04); --color-white-mute: #eee; --color-black: #1b1b1f; @@ -108,7 +108,7 @@ body[theme-mode='light'] { --color-text: var(--color-text-1); --color-icon: #00000099; --color-icon-white: #000000; - --color-border: #00000015; + --color-border: #00000019; --color-border-soft: #00000010; --color-border-mute: #00000005; --color-error: #f44336; diff --git a/src/renderer/src/components/CustomCollapse.tsx b/src/renderer/src/components/CustomCollapse.tsx index bcafec7a..9b0ba053 100644 --- a/src/renderer/src/components/CustomCollapse.tsx +++ b/src/renderer/src/components/CustomCollapse.tsx @@ -5,9 +5,19 @@ interface CustomCollapseProps { label: React.ReactNode extra: React.ReactNode children: React.ReactNode + destroyInactivePanel?: boolean + defaultActiveKey?: string[] + collapsible?: 'header' | 'icon' | 'disabled' } -const CustomCollapse: FC = ({ label, extra, children }) => { +const CustomCollapse: FC = ({ + label, + extra, + children, + destroyInactivePanel = false, + defaultActiveKey = ['1'], + collapsible = undefined +}) => { const CollapseStyle = { width: '100%', background: 'transparent', @@ -27,7 +37,9 @@ const CustomCollapse: FC = ({ label, extra, children }) => = ({ children, icon, color, size = 12, tooltip }) => { + return ( + + + {icon && icon} {children} + + + ) +} + +export default CustomTag + +const Tag = styled.div<{ $color: string; $size: number }>` + display: inline-flex; + align-items: center; + gap: 4px; + padding: ${({ $size }) => $size / 3}px ${({ $size }) => $size * 0.8}px; + border-radius: 99px; + color: ${({ $color }) => $color}; + background-color: ${({ $color }) => $color + '20'}; + font-size: ${({ $size }) => $size}px; + line-height: 1; + white-space: nowrap; + .iconfont { + font-size: ${({ $size }) => $size}px; + color: ${({ $color }) => $color}; + } +` diff --git a/src/renderer/src/components/Icons/SVGIcon.tsx b/src/renderer/src/components/Icons/SVGIcon.tsx new file mode 100644 index 00000000..e6521a97 --- /dev/null +++ b/src/renderer/src/components/Icons/SVGIcon.tsx @@ -0,0 +1,13 @@ +import { SVGProps } from 'react' + +export const StreamlineGoodHealthAndWellBeing = (props: SVGProps) => { + return ( + + {/* Icon from Streamline by Streamline - https://creativecommons.org/licenses/by/4.0/ */} + + + + + + ) +} diff --git a/src/renderer/src/components/ModelTagsWithLabel.tsx b/src/renderer/src/components/ModelTagsWithLabel.tsx new file mode 100644 index 00000000..de92fdd7 --- /dev/null +++ b/src/renderer/src/components/ModelTagsWithLabel.tsx @@ -0,0 +1,113 @@ +import { EyeOutlined, GlobalOutlined, ToolOutlined } from '@ant-design/icons' +import { + isEmbeddingModel, + isFunctionCallingModel, + isReasoningModel, + isRerankModel, + isVisionModel, + isWebSearchModel +} from '@renderer/config/models' +import i18n from '@renderer/i18n' +import { Model } from '@renderer/types' +import { isFreeModel } from '@renderer/utils' +import { FC, useEffect, useRef, useState } from 'react' +import { useTranslation } from 'react-i18next' +import styled from 'styled-components' + +import CustomTag from './CustomTag' + +interface ModelTagsProps { + model: Model + showFree?: boolean + showReasoning?: boolean + showToolsCalling?: boolean + size?: number + showLabel?: boolean +} + +const ModelTagsWithLabel: FC = ({ + model, + showFree = true, + showReasoning = true, + showToolsCalling = true, + size = 12, + showLabel = true +}) => { + const { t } = useTranslation() + const [_showLabel, _setShowLabel] = useState(showLabel) + const containerRef = useRef(null) + const resizeObserver = useRef(null) + + useEffect(() => { + if (!showLabel) return + + if (containerRef.current) { + const currentElement = containerRef.current + resizeObserver.current = new ResizeObserver((entries) => { + const maxWidth = i18n.language.startsWith('zh') ? 300 : 350 + + for (const entry of entries) { + const { width } = entry.contentRect + _setShowLabel(width >= maxWidth) + } + }) + resizeObserver.current.observe(currentElement) + + return () => { + if (resizeObserver.current) { + resizeObserver.current.unobserve(currentElement) + } + } + } + + return undefined + }, [showLabel]) + + return ( + + {isVisionModel(model) && ( + } tooltip={t('models.type.vision')}> + {_showLabel ? t('models.type.vision') : ''} + + )} + {isWebSearchModel(model) && ( + } tooltip={t('models.type.websearch')}> + {_showLabel ? t('models.type.websearch') : ''} + + )} + {showReasoning && isReasoningModel(model) && ( + } + tooltip={t('models.type.reasoning')}> + {_showLabel ? t('models.type.reasoning') : ''} + + )} + {showToolsCalling && isFunctionCallingModel(model) && ( + } tooltip={t('models.function_calling')}> + {_showLabel ? t('models.function_calling') : ''} + + )} + {isEmbeddingModel(model) && ( + + )} + {showFree && isFreeModel(model) && ( + + )} + {isRerankModel(model) && ( + + )} + + ) +} + +const Container = styled.div` + display: flex; + flex-direction: row; + align-items: center; + gap: 4px; + flex-wrap: wrap; +` + +export default ModelTagsWithLabel diff --git a/src/renderer/src/pages/files/FileItem.tsx b/src/renderer/src/pages/files/FileItem.tsx index 5aa0f7c2..13bc4f72 100644 --- a/src/renderer/src/pages/files/FileItem.tsx +++ b/src/renderer/src/pages/files/FileItem.tsx @@ -18,11 +18,13 @@ import styled from 'styled-components' interface FileItemProps { fileInfo: { + icon?: React.ReactNode name: React.ReactNode | string ext: string extra?: React.ReactNode | string actions: React.ReactNode } + style?: React.CSSProperties } const getFileIcon = (type?: string) => { @@ -73,18 +75,18 @@ const getFileIcon = (type?: string) => { return } -const FileItem: React.FC = ({ fileInfo }) => { - const { name, ext, extra, actions } = fileInfo +const FileItem: React.FC = ({ fileInfo, style }) => { + const { name, ext, extra, actions, icon } = fileInfo return ( - + - {getFileIcon(ext)} - + {icon || getFileIcon(ext)} + {name} {extra && {extra}} - {actions} + {actions} ) @@ -96,7 +98,9 @@ const FileItemCard = styled.div` overflow: hidden; border: 0.5px solid var(--color-border); flex-shrink: 0; - transition: box-shadow 0.2s ease; + transition: + box-shadow 0.2s ease, + background-color 0.2s ease; --shadow-color: rgba(0, 0, 0, 0.05); &:hover { box-shadow: @@ -109,15 +113,19 @@ const FileItemCard = styled.div` ` const CardContent = styled.div` - padding: 8px 16px; + padding: 8px 8px 8px 16px; display: flex; - align-items: center; + align-items: stretch; gap: 16px; ` const FileIcon = styled.div` + max-height: 44px; color: var(--color-text-3); font-size: 32px; + display: flex; + align-items: center; + justify-content: center; ` const FileName = styled.div` @@ -140,4 +148,11 @@ const FileInfo = styled.div` color: var(--color-text-2); ` +const FileActions = styled.div` + max-height: 44px; + display: flex; + align-items: center; + justify-content: center; +` + export default memo(FileItem) diff --git a/src/renderer/src/pages/files/FileList.tsx b/src/renderer/src/pages/files/FileList.tsx index 41b1ebb1..e00c7cbb 100644 --- a/src/renderer/src/pages/files/FileList.tsx +++ b/src/renderer/src/pages/files/FileList.tsx @@ -66,7 +66,7 @@ const FileList: React.FC = ({ id, list, files }) => { = ({ id, list, files }) => { {(item) => (
= ({ ref, mentionModels, onMentionModel, To .reverse() .map((item) => ({ label: `${item.provider.isSystem ? t(`provider.${item.provider.id}`) : item.provider.name} | ${item.model.name}`, - description: , + description: , icon: ( {first(item.model.name)} diff --git a/src/renderer/src/pages/home/Tabs/TopicsTab.tsx b/src/renderer/src/pages/home/Tabs/TopicsTab.tsx index 8ff853e1..000294a3 100644 --- a/src/renderer/src/pages/home/Tabs/TopicsTab.tsx +++ b/src/renderer/src/pages/home/Tabs/TopicsTab.tsx @@ -493,7 +493,6 @@ const TopicListItem = styled.div` } .menu { opacity: 1; - background-color: var(--color-background-soft); &:hover { color: var(--color-text-2); } diff --git a/src/renderer/src/pages/knowledge/KnowledgeContent.tsx b/src/renderer/src/pages/knowledge/KnowledgeContent.tsx index 2b582432..866bb20f 100644 --- a/src/renderer/src/pages/knowledge/KnowledgeContent.tsx +++ b/src/renderer/src/pages/knowledge/KnowledgeContent.tsx @@ -7,6 +7,7 @@ import { SearchOutlined, SettingOutlined } from '@ant-design/icons' +import CustomTag from '@renderer/components/CustomTag' import Ellipsis from '@renderer/components/Ellipsis' import { HStack } from '@renderer/components/Layout' import PromptPopup from '@renderer/components/Popups/PromptPopup' @@ -18,7 +19,7 @@ import { getProviderName } from '@renderer/services/ProviderService' import { FileType, FileTypes, KnowledgeBase, KnowledgeItem } from '@renderer/types' import { formatFileSize } from '@renderer/utils' import { bookExts, documentExts, textExts, thirdPartyApplicationExts } from '@shared/config/constant' -import { Alert, Button, Dropdown, Empty, message, Tag, Tooltip, Upload } from 'antd' +import { Alert, Button, Dropdown, Empty, Flex, message, Tooltip, Upload } from 'antd' import dayjs from 'dayjs' import VirtualList from 'rc-virtual-list' import { FC } from 'react' @@ -269,8 +270,8 @@ const KnowledgeContent: FC = ({ selectedBase }) => { ) : ( 5 ? 400 : fileItems.length * 80} - itemHeight={80} + height={fileItems.length > 5 ? 400 : fileItems.length * 75} + itemHeight={75} itemKey="id" styles={{ verticalScrollBar: { @@ -283,7 +284,7 @@ const KnowledgeContent: FC = ({ selectedBase }) => { {(item) => { const file = item.content as FileType return ( -
+
= ({ selectedBase }) => { ))} - -
- -
-
-
- -
-
- {providerName && {providerName}} - {base.model.name} - {t('models.dimensions', { dimensions: base.dimensions || 0 })} -
-
- - {base.rerankModel && ( + + {t('knowledge.model_info')} + + }> +
- +
- {rerankModelProviderName && {rerankModelProviderName}} - {base.rerankModel?.name} + {providerName && {providerName}} + {base.model.name} + {t('models.dimensions', { dimensions: base.dimensions || 0 })}
- )} -
- - - + {base.rerankModel && ( +
+
+ +
+
+ {rerankModelProviderName && {rerankModelProviderName}} + {base.rerankModel?.name} +
+
+ )} +
+ @@ -588,9 +599,9 @@ const CollapseLabel = ({ label, count }: { label: string; count: number }) => { return ( - + {count} - + ) } @@ -605,12 +616,6 @@ const MainContent = styled(Scrollbar)` gap: 16px; ` -const IndexSection = styled.div` - margin-top: 20px; - display: flex; - justify-content: center; -` - const ModelInfo = styled.div` display: flex; flex-direction: column; @@ -629,6 +634,7 @@ const ModelInfo = styled.div` display: flex; align-items: flex-start; gap: 10px; + margin-top: 16px; } .label-column { diff --git a/src/renderer/src/pages/settings/ProviderSettings/AddModelPopup.tsx b/src/renderer/src/pages/settings/ProviderSettings/AddModelPopup.tsx index d45c9c9d..6b45ac78 100644 --- a/src/renderer/src/pages/settings/ProviderSettings/AddModelPopup.tsx +++ b/src/renderer/src/pages/settings/ProviderSettings/AddModelPopup.tsx @@ -120,13 +120,11 @@ const PopupContainer: React.FC = ({ title, provider, resolve }) => { tooltip={t('settings.models.add.group_name.tooltip')}> - - -
- -
+ + + diff --git a/src/renderer/src/pages/settings/ProviderSettings/EditModelsPopup.tsx b/src/renderer/src/pages/settings/ProviderSettings/EditModelsPopup.tsx index ab094599..8766df3a 100644 --- a/src/renderer/src/pages/settings/ProviderSettings/EditModelsPopup.tsx +++ b/src/renderer/src/pages/settings/ProviderSettings/EditModelsPopup.tsx @@ -1,6 +1,7 @@ -import { LoadingOutlined, MinusOutlined, PlusOutlined, QuestionCircleOutlined } from '@ant-design/icons' -import { Center } from '@renderer/components/Layout' -import ModelTags from '@renderer/components/ModelTags' +import { LoadingOutlined, MinusOutlined, PlusOutlined, SearchOutlined } from '@ant-design/icons' +import CustomCollapse from '@renderer/components/CustomCollapse' +import CustomTag from '@renderer/components/CustomTag' +import ModelTagsWithLabel from '@renderer/components/ModelTagsWithLabel' import { getModelLogo, isEmbeddingModel, @@ -12,11 +13,12 @@ import { SYSTEM_MODELS } from '@renderer/config/models' import { useProvider } from '@renderer/hooks/useProvider' +import FileItem from '@renderer/pages/files/FileItem' import { fetchModels } from '@renderer/services/ApiService' import { Model, Provider } from '@renderer/types' import { getDefaultGroupName, isFreeModel, runAsyncFunction } from '@renderer/utils' -import { Avatar, Button, Empty, Flex, Modal, Popover, Radio, Tooltip } from 'antd' -import Search from 'antd/es/input/Search' +import { Avatar, Button, Empty, Flex, Modal, Tabs, Tooltip, Typography } from 'antd' +import Input from 'antd/es/input/Input' import { groupBy, isEmpty, uniqBy } from 'lodash' import { useEffect, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' @@ -163,51 +165,63 @@ const PopupContainer: React.FC = ({ provider: _provider, resolve }) => { width="680px" styles={{ content: { padding: 0 }, - header: { padding: 22, paddingBottom: 15 } + header: { padding: '16px 22px 30px 22px' } }} centered> -
- setFilterType(e.target.value)} - buttonStyle="solid"> - {t('models.all')} - {t('models.type.reasoning')} - {t('models.type.vision')} - {t('models.type.websearch')} - {t('models.type.free')} - {t('models.type.embedding')} - {t('models.type.rerank')} - {t('models.type.function_calling')} - -
- } + size="large" ref={searchInputRef} placeholder={t('settings.provider.search_placeholder')} allowClear onChange={(e) => setSearchText(e.target.value)} - onSearch={setSearchText} + /> + setFilterType(key)} />
- {Object.keys(modelGroups).map((group) => { + {Object.keys(modelGroups).map((group, i) => { const isAllInProvider = modelGroups[group].every((model) => isModelInProvider(provider, model.id)) return ( -
- - {group} -
+ = 5 ? [] : ['1']} + label={ + + {group} + + {modelGroups[group].length} + + + } + extra={ +
-
- {modelGroups[group].map((model) => { - return ( - - - - {model?.name?.[0]?.toUpperCase()} - - - - {model.name} - - - {!isEmpty(model.description) && ( - - - - )} - - - {isModelInProvider(provider, model.id) ? ( -
+ + }> + + {modelGroups[group].map((model) => ( + {model?.name?.[0]?.toUpperCase()}, + name: ( + + + {model.id} + + } + placement="top"> + {model.name} + + + ), + extra: ( +
+ + + {model.description && ( + + {model.description} + + )} +
+ ), + ext: '.model', + actions: ( +
+ {isModelInProvider(provider, model.id) ? ( +
+ ) + }} + /> + ))} +
+ ) })} {isEmpty(list) && } @@ -264,7 +306,6 @@ const SearchContainer = styled.div` flex-direction: column; gap: 15px; padding: 0 22px; - padding-bottom: 10px; margin-top: -10px; .ant-radio-group { @@ -274,37 +315,21 @@ const SearchContainer = styled.div` ` const ListContainer = styled.div` - max-height: 70vh; + height: calc(100vh - 300px); overflow-y: scroll; - padding-bottom: 20px; -` - -const ListHeader = styled.div` - display: flex; - flex-direction: row; - align-items: center; - justify-content: space-between; - background-color: var(--color-background-soft); - padding: 8px 22px; - color: var(--color-text); - opacity: 0.4; -` - -const ListItem = styled.div` - display: flex; - flex-direction: row; - align-items: center; - justify-content: space-between; - padding: 10px 22px; -` - -const ListItemHeader = styled.div` - display: flex; - flex-direction: row; - align-items: center; - justify-content: flex-start; + padding: 0 6px 16px 6px; + margin-left: 16px; margin-right: 10px; - height: 22px; + display: flex; + flex-direction: column; + gap: 16px; +` + +const FlexColumn = styled.div` + display: flex; + flex-direction: column; + gap: 12px; + margin-top: 16px; ` const ListItemName = styled.div` @@ -314,8 +339,8 @@ const ListItemName = styled.div` gap: 10px; color: var(--color-text); font-size: 14px; + line-height: 1; font-weight: 600; - margin-left: 6px; ` const ModelHeaderTitle = styled.div` @@ -325,11 +350,6 @@ const ModelHeaderTitle = styled.div` margin-right: 10px; ` -const Question = styled(QuestionCircleOutlined)` - cursor: pointer; - color: #888; -` - export default class EditModelsPopup { static topviewId = 0 static hide() { diff --git a/src/renderer/src/pages/settings/ProviderSettings/HealthCheckPopup.tsx b/src/renderer/src/pages/settings/ProviderSettings/HealthCheckPopup.tsx index 22aac71c..839dc1fd 100644 --- a/src/renderer/src/pages/settings/ProviderSettings/HealthCheckPopup.tsx +++ b/src/renderer/src/pages/settings/ProviderSettings/HealthCheckPopup.tsx @@ -160,7 +160,7 @@ const PopupContainer: React.FC = ({ title, apiKeys, resolve }) => { /> - diff --git a/src/renderer/src/pages/settings/ProviderSettings/ModelEditContent.tsx b/src/renderer/src/pages/settings/ProviderSettings/ModelEditContent.tsx index 2372e041..7d458dbb 100644 --- a/src/renderer/src/pages/settings/ProviderSettings/ModelEditContent.tsx +++ b/src/renderer/src/pages/settings/ProviderSettings/ModelEditContent.tsx @@ -102,18 +102,14 @@ const ModelEditContent: FC = ({ model, onUpdateModel, ope
- -
- -
- setShowModelTypes(!showModelTypes)} - style={{ position: 'absolute', right: 0 }}> + + setShowModelTypes(!showModelTypes)}> {t('settings.moresetting')} {showModelTypes ? : } +
{showModelTypes && ( diff --git a/src/renderer/src/pages/settings/ProviderSettings/ModelList.tsx b/src/renderer/src/pages/settings/ProviderSettings/ModelList.tsx index b461332a..d9f8e1e7 100644 --- a/src/renderer/src/pages/settings/ProviderSettings/ModelList.tsx +++ b/src/renderer/src/pages/settings/ProviderSettings/ModelList.tsx @@ -5,20 +5,24 @@ import { ExclamationCircleFilled, LoadingOutlined, MinusCircleOutlined, + MinusOutlined, PlusOutlined, SettingOutlined } from '@ant-design/icons' -import ModelTags from '@renderer/components/ModelTags' +import CustomCollapse from '@renderer/components/CustomCollapse' +import CustomTag from '@renderer/components/CustomTag' +import ModelTagsWithLabel from '@renderer/components/ModelTagsWithLabel' import { getModelLogo } from '@renderer/config/models' import { PROVIDER_CONFIG } from '@renderer/config/providers' import { useAssistants, useDefaultModel } from '@renderer/hooks/useAssistant' import { useProvider } from '@renderer/hooks/useProvider' +import FileItem from '@renderer/pages/files/FileItem' import { ModelCheckStatus } from '@renderer/services/HealthCheckService' import { useAppDispatch } from '@renderer/store' import { setModel } from '@renderer/store/assistants' import { Model } from '@renderer/types' import { maskApiKey } from '@renderer/utils/api' -import { Avatar, Button, Card, Flex, Space, Tooltip, Typography } from 'antd' +import { Avatar, Button, Flex, Tooltip, Typography } from 'antd' import { groupBy, sortBy, toPairs } from 'lodash' import React, { memo, useCallback, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' @@ -240,71 +244,107 @@ const ModelList: React.FC = ({ providerId, modelStatuses = [], s return ( <> - {Object.keys(sortedModelGroups).map((group) => ( - - - modelGroups[group] - .filter((model) => provider.models.some((m) => m.id === model.id)) - .forEach((model) => removeModel(model)) - } - /> - - } - style={{ marginBottom: '10px', border: '0.5px solid var(--color-border)' }} - size="small"> - {sortedModelGroups[group].map((model) => { - const modelStatus = modelStatuses.find((status) => status.model.id === model.id) - const isChecking = modelStatus?.checking === true - console.log('model', model.id, getModelLogo(model.id)) + + {Object.keys(sortedModelGroups).map((group, i) => ( + + {group} + + {modelGroups[group].length} + + + } + extra={ + + + modelGroups[group] + .filter((model) => provider.models.some((m) => m.id === model.id)) + .forEach((model) => removeModel(model)) + } + /> + + }> + + {sortedModelGroups[group].map((model) => { + const modelStatus = modelStatuses.find((status) => status.model.id === model.id) + const isChecking = modelStatus?.checking === true - return ( - - - - {model?.name?.[0]?.toUpperCase()} - - - {model?.name} - - - !isChecking && onEditModel(model)} - style={{ cursor: isChecking ? 'not-allowed' : 'pointer', opacity: isChecking ? 0.5 : 1 }} + return ( + {model?.name?.[0]?.toUpperCase()}, + name: ( + + + {model.id} + + } + placement="top"> + {model.name} + + + ), + extra: ( +
+ +
+ ), + ext: '.model', + actions: ( + + {renderLatencyText(modelStatus)} + {renderStatusIndicator(modelStatus)} + +