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 { Model } from '@renderer/types'
|
||||||
import { Avatar, Divider, Empty, Input, InputRef, Menu, MenuProps, Modal } from 'antd'
|
import { Avatar, Divider, Empty, Input, InputRef, Menu, MenuProps, Modal } from 'antd'
|
||||||
import { first, sortBy } from 'lodash'
|
import { first, sortBy } from 'lodash'
|
||||||
import { useEffect, useRef, useState } from 'react'
|
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
|
|
||||||
@ -33,6 +33,7 @@ const PopupContainer: React.FC<PopupContainerProps> = ({ model, resolve }) => {
|
|||||||
const { providers } = useProviders()
|
const { providers } = useProviders()
|
||||||
const [pinnedModels, setPinnedModels] = useState<string[]>([])
|
const [pinnedModels, setPinnedModels] = useState<string[]>([])
|
||||||
const scrollContainerRef = useRef<HTMLDivElement>(null)
|
const scrollContainerRef = useRef<HTMLDivElement>(null)
|
||||||
|
const [keyboardSelectedId, setKeyboardSelectedId] = useState<string>('')
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const loadPinnedModels = async () => {
|
const loadPinnedModels = async () => {
|
||||||
@ -153,10 +154,12 @@ const PopupContainer: React.FC<PopupContainerProps> = ({ model, resolve }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const onCancel = () => {
|
const onCancel = () => {
|
||||||
|
setKeyboardSelectedId('')
|
||||||
setOpen(false)
|
setOpen(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
const onClose = async () => {
|
const onClose = async () => {
|
||||||
|
setKeyboardSelectedId('')
|
||||||
resolve(undefined)
|
resolve(undefined)
|
||||||
SelectModelPopup.hide()
|
SelectModelPopup.hide()
|
||||||
}
|
}
|
||||||
@ -176,6 +179,90 @@ const PopupContainer: React.FC<PopupContainerProps> = ({ model, resolve }) => {
|
|||||||
}
|
}
|
||||||
}, [open, model])
|
}, [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 (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
centered
|
centered
|
||||||
@ -210,18 +297,19 @@ const PopupContainer: React.FC<PopupContainerProps> = ({ model, resolve }) => {
|
|||||||
style={{ paddingLeft: 0 }}
|
style={{ paddingLeft: 0 }}
|
||||||
bordered={false}
|
bordered={false}
|
||||||
size="middle"
|
size="middle"
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
// 防止上下键移动光标
|
||||||
|
if (e.key === 'ArrowUp' || e.key === 'ArrowDown') {
|
||||||
|
e.preventDefault()
|
||||||
|
}
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</HStack>
|
</HStack>
|
||||||
<Divider style={{ margin: 0, borderBlockStartWidth: 0.5 }} />
|
<Divider style={{ margin: 0, borderBlockStartWidth: 0.5 }} />
|
||||||
<Scrollbar style={{ height: '50vh' }} ref={scrollContainerRef}>
|
<Scrollbar style={{ height: '50vh' }} ref={scrollContainerRef}>
|
||||||
<Container>
|
<Container>
|
||||||
{filteredItems.length > 0 ? (
|
{filteredItems.length > 0 ? (
|
||||||
<StyledMenu
|
<StyledMenu items={filteredItems} selectedKeys={[keyboardSelectedId]} mode="inline" inlineIndent={6} />
|
||||||
items={filteredItems}
|
|
||||||
selectedKeys={model ? [getModelUniqId(model)] : []}
|
|
||||||
mode="inline"
|
|
||||||
inlineIndent={6}
|
|
||||||
/>
|
|
||||||
) : (
|
) : (
|
||||||
<EmptyState>
|
<EmptyState>
|
||||||
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} />
|
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} />
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user