From c0117c25acbe4b1eb50bf8debdd5e78216a49e16 Mon Sep 17 00:00:00 2001 From: CherryLover Date: Thu, 27 Feb 2025 16:58:11 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E4=B8=BA=E6=A8=A1=E5=9E=8B=E9=80=89?= =?UTF-8?q?=E6=8B=A9=E5=BC=B9=E7=AA=97=E6=B7=BB=E5=8A=A0=E9=94=AE=E7=9B=98?= =?UTF-8?q?=E5=AF=BC=E8=88=AA=E5=8A=9F=E8=83=BD=20(#2057)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 为模型选择弹窗添加键盘导航功能 * fix(SelectModelPopup): 改进键盘导航和项目筛选 --- .../components/Popups/SelectModelPopup.tsx | 102 ++++++++++++++++-- 1 file changed, 95 insertions(+), 7 deletions(-) diff --git a/src/renderer/src/components/Popups/SelectModelPopup.tsx b/src/renderer/src/components/Popups/SelectModelPopup.tsx index 01534c67..e985c8ba 100644 --- a/src/renderer/src/components/Popups/SelectModelPopup.tsx +++ b/src/renderer/src/components/Popups/SelectModelPopup.tsx @@ -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 = ({ model, resolve }) => { const { providers } = useProviders() const [pinnedModels, setPinnedModels] = useState([]) const scrollContainerRef = useRef(null) + const [keyboardSelectedId, setKeyboardSelectedId] = useState('') useEffect(() => { const loadPinnedModels = async () => { @@ -153,10 +154,12 @@ const PopupContainer: React.FC = ({ model, resolve }) => { } const onCancel = () => { + setKeyboardSelectedId('') setOpen(false) } const onClose = async () => { + setKeyboardSelectedId('') resolve(undefined) SelectModelPopup.hide() } @@ -176,6 +179,90 @@ const PopupContainer: React.FC = ({ 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 ( = ({ model, resolve }) => { style={{ paddingLeft: 0 }} bordered={false} size="middle" + onKeyDown={(e) => { + // 防止上下键移动光标 + if (e.key === 'ArrowUp' || e.key === 'ArrowDown') { + e.preventDefault() + } + }} /> {filteredItems.length > 0 ? ( - + ) : (