feat: 为模型选择弹窗添加键盘导航功能 (#2057)
* feat: 为模型选择弹窗添加键盘导航功能 * fix(SelectModelPopup): 改进键盘导航和项目筛选
This commit is contained in:
parent
d51da99b8f
commit
c0117c25ac
@ -7,7 +7,7 @@ import { getModelUniqId } from '@renderer/services/ModelService'
|
||||
import { Model } from '@renderer/types'
|
||||
import { Avatar, Divider, Empty, Input, InputRef, Menu, MenuProps, Modal } from 'antd'
|
||||
import { first, sortBy } from 'lodash'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
|
||||
@ -33,6 +33,7 @@ const PopupContainer: React.FC<PopupContainerProps> = ({ model, resolve }) => {
|
||||
const { providers } = useProviders()
|
||||
const [pinnedModels, setPinnedModels] = useState<string[]>([])
|
||||
const scrollContainerRef = useRef<HTMLDivElement>(null)
|
||||
const [keyboardSelectedId, setKeyboardSelectedId] = useState<string>('')
|
||||
|
||||
useEffect(() => {
|
||||
const loadPinnedModels = async () => {
|
||||
@ -153,10 +154,12 @@ const PopupContainer: React.FC<PopupContainerProps> = ({ model, resolve }) => {
|
||||
}
|
||||
|
||||
const onCancel = () => {
|
||||
setKeyboardSelectedId('')
|
||||
setOpen(false)
|
||||
}
|
||||
|
||||
const onClose = async () => {
|
||||
setKeyboardSelectedId('')
|
||||
resolve(undefined)
|
||||
SelectModelPopup.hide()
|
||||
}
|
||||
@ -176,6 +179,90 @@ const PopupContainer: React.FC<PopupContainerProps> = ({ model, resolve }) => {
|
||||
}
|
||||
}, [open, model])
|
||||
|
||||
// 获取所有可见的模型项
|
||||
const getVisibleModelItems = useCallback(() => {
|
||||
const items: { key: string; model: Model }[] = []
|
||||
|
||||
// 如果有置顶模型且没有搜索文本,添加置顶模型
|
||||
if (pinnedModels.length > 0 && searchText.length === 0) {
|
||||
providers
|
||||
.flatMap((p) => p.models || [])
|
||||
.filter((m) => pinnedModels.includes(getModelUniqId(m)))
|
||||
.forEach((m) => items.push({ key: getModelUniqId(m) + '_pinned', model: m }))
|
||||
}
|
||||
|
||||
// 添加其他过滤后的模型
|
||||
providers.forEach((p) => {
|
||||
if (p.models) {
|
||||
sortBy(p.models, ['group', 'name'])
|
||||
.filter((m) => !isEmbeddingModel(m))
|
||||
.filter((m) =>
|
||||
[m.name + m.provider + t('provider.' + p.id)].join('').toLowerCase().includes(searchText.toLowerCase())
|
||||
)
|
||||
.forEach((m) => {
|
||||
const modelId = getModelUniqId(m)
|
||||
const isPinned = pinnedModels.includes(modelId)
|
||||
// 如果是搜索状态,或者不是固定模型,才添加到列表中
|
||||
if (searchText.length > 0 || !isPinned) {
|
||||
items.push({
|
||||
key: isPinned ? modelId + '_pinned' : modelId,
|
||||
model: m
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
return items
|
||||
}, [pinnedModels, searchText, providers, t])
|
||||
|
||||
// 处理键盘导航
|
||||
const handleKeyDown = useCallback(
|
||||
(e: KeyboardEvent) => {
|
||||
const items = getVisibleModelItems()
|
||||
if (items.length === 0) return
|
||||
|
||||
if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {
|
||||
e.preventDefault()
|
||||
const currentIndex = items.findIndex((item) => item.key === keyboardSelectedId)
|
||||
let nextIndex
|
||||
|
||||
if (currentIndex === -1) {
|
||||
nextIndex = e.key === 'ArrowDown' ? 0 : items.length - 1
|
||||
} else {
|
||||
nextIndex =
|
||||
e.key === 'ArrowDown' ? (currentIndex + 1) % items.length : (currentIndex - 1 + items.length) % items.length
|
||||
}
|
||||
|
||||
const nextItem = items[nextIndex]
|
||||
setKeyboardSelectedId(nextItem.key)
|
||||
|
||||
const element = document.querySelector(`[data-menu-id="${nextItem.key}"]`)
|
||||
element?.scrollIntoView({ block: 'nearest', behavior: 'smooth' })
|
||||
} else if (e.key === 'Enter') {
|
||||
e.preventDefault() // 阻止回车的默认行为
|
||||
if (keyboardSelectedId) {
|
||||
const selectedItem = items.find((item) => item.key === keyboardSelectedId)
|
||||
if (selectedItem) {
|
||||
resolve(selectedItem.model)
|
||||
setOpen(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
[keyboardSelectedId, getVisibleModelItems, resolve, setOpen]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener('keydown', handleKeyDown)
|
||||
return () => window.removeEventListener('keydown', handleKeyDown)
|
||||
}, [handleKeyDown])
|
||||
|
||||
// 搜索文本改变时重置键盘选中状态
|
||||
useEffect(() => {
|
||||
setKeyboardSelectedId('')
|
||||
}, [searchText])
|
||||
|
||||
return (
|
||||
<Modal
|
||||
centered
|
||||
@ -210,18 +297,19 @@ const PopupContainer: React.FC<PopupContainerProps> = ({ model, resolve }) => {
|
||||
style={{ paddingLeft: 0 }}
|
||||
bordered={false}
|
||||
size="middle"
|
||||
onKeyDown={(e) => {
|
||||
// 防止上下键移动光标
|
||||
if (e.key === 'ArrowUp' || e.key === 'ArrowDown') {
|
||||
e.preventDefault()
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</HStack>
|
||||
<Divider style={{ margin: 0, borderBlockStartWidth: 0.5 }} />
|
||||
<Scrollbar style={{ height: '50vh' }} ref={scrollContainerRef}>
|
||||
<Container>
|
||||
{filteredItems.length > 0 ? (
|
||||
<StyledMenu
|
||||
items={filteredItems}
|
||||
selectedKeys={model ? [getModelUniqId(model)] : []}
|
||||
mode="inline"
|
||||
inlineIndent={6}
|
||||
/>
|
||||
<StyledMenu items={filteredItems} selectedKeys={[keyboardSelectedId]} mode="inline" inlineIndent={6} />
|
||||
) : (
|
||||
<EmptyState>
|
||||
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} />
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user