perf: improve model list loading (#8751)

* refactor(ModelList): use spin as a wrapper

* perf: improve SvgSpinners180Ring
This commit is contained in:
one 2025-08-01 14:57:27 +08:00 committed by GitHub
parent 488a01d7d7
commit e2b13ade95
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 91 additions and 62 deletions

View File

@ -1,20 +1,41 @@
import { SVGProps } from 'react'
export function SvgSpinners180Ring(props: SVGProps<SVGSVGElement>) {
// 避免与全局样式冲突
const animationClassName = 'svg-spinner-anim-180-ring'
return (
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" {...props}>
{/* Icon from SVG Spinners by Utkarsh Verma - https://github.com/n3r4zzurr0/svg-spinners/blob/main/LICENSE */}
<path
fill="currentColor"
d="M12,4a8,8,0,0,1,7.89,6.7A1.53,1.53,0,0,0,21.38,12h0a1.5,1.5,0,0,0,1.48-1.75,11,11,0,0,0-21.72,0A1.5,1.5,0,0,0,2.62,12h0a1.53,1.53,0,0,0,1.49-1.3A8,8,0,0,1,12,4Z">
<animateTransform
attributeName="transform"
dur="0.75s"
repeatCount="indefinite"
type="rotate"
values="0 12 12;360 12 12"></animateTransform>
</path>
</svg>
<>
{/* CSS transform 硬件加速 */}
<style>
{`
@keyframes svg-spinner-rotate-180-ring {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
.${animationClassName} {
transform-origin: center;
animation: svg-spinner-rotate-180-ring 0.75s linear infinite;
}
`}
</style>
<svg
xmlns="http://www.w3.org/2000/svg"
width="1em"
height="1em"
viewBox="0 0 24 24"
{...props}
className={`${animationClassName} ${props.className || ''}`.trim()}>
{/* Icon from SVG Spinners by Utkarsh Verma - https://github.com/n3r4zzurr0/svg-spinners/blob/main/LICENSE */}
<path
fill="currentColor"
d="M12,4a8,8,0,0,1,7.89,6.7A1.53,1.53,0,0,0,21.38,12h0a1.5,1.5,0,0,0,1.48-1.75,11,11,0,0,0-21.72,0A1.5,1.5,0,0,0,2.62,12h0a1.53,1.53,0,0,0,1.49-1.3A8,8,0,0,1,12,4Z"></path>
</svg>
</>
)
}
export default SvgSpinners180Ring

View File

@ -16,8 +16,8 @@ import { useAppDispatch } from '@renderer/store'
import { setModel } from '@renderer/store/assistants'
import { Model } from '@renderer/types'
import { filterModelsByKeywords } from '@renderer/utils'
import { Button, Flex, Spin, Tooltip } from 'antd'
import { groupBy, sortBy, toPairs } from 'lodash'
import { Button, Empty, Flex, Spin, Tooltip } from 'antd'
import { groupBy, isEmpty, sortBy, toPairs } from 'lodash'
import { ListCheck, Plus } from 'lucide-react'
import React, { memo, startTransition, useCallback, useEffect, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
@ -134,6 +134,8 @@ const ModelList: React.FC<ModelListProps> = ({ providerId }) => {
[provider, onUpdateModel]
)
const isLoading = useMemo(() => displayedModelGroups === null, [displayedModelGroups])
return (
<>
<SettingSubtitle style={{ marginBottom: 5 }}>
@ -158,54 +160,60 @@ const ModelList: React.FC<ModelListProps> = ({ providerId }) => {
</HStack>
</HStack>
</SettingSubtitle>
{displayedModelGroups === null ? (
<Flex align="center" justify="center" style={{ minHeight: '8rem' }}>
<Spin indicator={<SvgSpinners180Ring color="var(--color-text-2)" />} />
<Spin spinning={isLoading} indicator={<SvgSpinners180Ring color="var(--color-text-2)" />}>
{displayedModelGroups && !isEmpty(displayedModelGroups) ? (
<Flex gap={12} vertical>
{Object.keys(displayedModelGroups).map((group, i) => (
<ModelListGroup
key={group}
groupName={group}
models={displayedModelGroups[group]}
modelStatuses={modelStatuses}
defaultOpen={i <= 5}
disabled={isHealthChecking}
onEditModel={onEditModel}
onRemoveModel={removeModel}
onRemoveGroup={() => displayedModelGroups[group].forEach((model) => removeModel(model))}
/>
))}
</Flex>
) : (
<Empty
image={Empty.PRESENTED_IMAGE_SIMPLE}
description={t('settings.models.empty')}
style={{ visibility: isLoading ? 'hidden' : 'visible' }}
/>
)}
</Spin>
<Flex justify="space-between" align="center">
{docsWebsite || modelsWebsite ? (
<SettingHelpTextRow>
<SettingHelpText>{t('settings.provider.docs_check')} </SettingHelpText>
{docsWebsite && (
<SettingHelpLink target="_blank" href={docsWebsite}>
{getProviderLabel(provider.id) + ' '}
{t('common.docs')}
</SettingHelpLink>
)}
{docsWebsite && modelsWebsite && <SettingHelpText>{t('common.and')}</SettingHelpText>}
{modelsWebsite && (
<SettingHelpLink target="_blank" href={modelsWebsite}>
{t('common.models')}
</SettingHelpLink>
)}
<SettingHelpText>{t('settings.provider.docs_more_details')}</SettingHelpText>
</SettingHelpTextRow>
) : (
<div style={{ height: 5 }} />
)}
<Flex gap={10} style={{ marginTop: 12 }}>
<Button type="primary" onClick={onManageModel} icon={<ListCheck size={16} />} disabled={isHealthChecking}>
{t('button.manage')}
</Button>
<Button type="default" onClick={onAddModel} icon={<Plus size={16} />} disabled={isHealthChecking}>
{t('button.add')}
</Button>
</Flex>
) : (
<Flex gap={12} vertical>
{Object.keys(displayedModelGroups).map((group, i) => (
<ModelListGroup
key={group}
groupName={group}
models={displayedModelGroups[group]}
modelStatuses={modelStatuses}
defaultOpen={i <= 5}
disabled={isHealthChecking}
onEditModel={onEditModel}
onRemoveModel={removeModel}
onRemoveGroup={() => displayedModelGroups[group].forEach((model) => removeModel(model))}
/>
))}
{docsWebsite || modelsWebsite ? (
<SettingHelpTextRow>
<SettingHelpText>{t('settings.provider.docs_check')} </SettingHelpText>
{docsWebsite && (
<SettingHelpLink target="_blank" href={docsWebsite}>
{getProviderLabel(provider.id) + ' '}
{t('common.docs')}
</SettingHelpLink>
)}
{docsWebsite && modelsWebsite && <SettingHelpText>{t('common.and')}</SettingHelpText>}
{modelsWebsite && (
<SettingHelpLink target="_blank" href={modelsWebsite}>
{t('common.models')}
</SettingHelpLink>
)}
<SettingHelpText>{t('settings.provider.docs_more_details')}</SettingHelpText>
</SettingHelpTextRow>
) : (
<div style={{ height: 5 }} />
)}
</Flex>
)}
<Flex gap={10} style={{ marginTop: 10 }}>
<Button type="primary" onClick={onManageModel} icon={<ListCheck size={16} />} disabled={isHealthChecking}>
{t('button.manage')}
</Button>
<Button type="default" onClick={onAddModel} icon={<Plus size={16} />} disabled={isHealthChecking}>
{t('button.add')}
</Button>
</Flex>
</>
)