From 202504fd172d832831a7459349401fa6d498b565 Mon Sep 17 00:00:00 2001 From: one Date: Mon, 16 Jun 2025 12:35:24 +0800 Subject: [PATCH] refactor(Navbar&Sidebar): rearrange navbar icons, add search functionality for assistants and topics (#7170) * refactor(Navbar&Sidebar): rearrange navbar icons, add search functionality for assistants and topics - Updated ChatNavbar and MainNavbar to streamline the display of assistant icons based on their visibility state. - Introduced search input in MainSidebar for filtering assistants and topics. - Enhanced AssistantsTab and TopicsTab to support search functionality, allowing users to filter displayed items based on input. - Added a new utility function to improve search keyword matching logic. - Improved overall layout and styling for better user experience. * refactor: update icons * refactor(Search): allow clear * refactor: enhance search bar * refactor: improve search bar style * feat: new panel left icon --- .../src/components/Icons/NarrowModeIcon.tsx | 2 +- .../src/components/Icons/PanelIcons.tsx | 42 +++++++ src/renderer/src/pages/home/ChatNavbar.tsx | 23 ++-- .../src/pages/home/MainSidebar/MainNavbar.tsx | 26 ++--- .../pages/home/MainSidebar/MainSidebar.tsx | 11 +- .../pages/home/MainSidebar/SidebarSearch.tsx | 106 ++++++++++++++++++ .../src/pages/home/Tabs/AssistantsTab.tsx | 34 +++++- .../src/pages/home/Tabs/TopicsTab.tsx | 14 ++- src/renderer/src/utils/search.ts | 20 ++++ 9 files changed, 240 insertions(+), 38 deletions(-) create mode 100644 src/renderer/src/components/Icons/PanelIcons.tsx create mode 100644 src/renderer/src/pages/home/MainSidebar/SidebarSearch.tsx create mode 100644 src/renderer/src/utils/search.ts diff --git a/src/renderer/src/components/Icons/NarrowModeIcon.tsx b/src/renderer/src/components/Icons/NarrowModeIcon.tsx index ac3bc01513..6a1eb3844e 100644 --- a/src/renderer/src/components/Icons/NarrowModeIcon.tsx +++ b/src/renderer/src/components/Icons/NarrowModeIcon.tsx @@ -26,7 +26,7 @@ const Container = styled.div<{ $isNarrowMode: boolean }>` ` const Line = styled.div` - width: 1.5px; + width: 2px; height: 10px; background-color: var(--color-text-2); border-radius: 5px; diff --git a/src/renderer/src/components/Icons/PanelIcons.tsx b/src/renderer/src/components/Icons/PanelIcons.tsx new file mode 100644 index 0000000000..43858e49f5 --- /dev/null +++ b/src/renderer/src/components/Icons/PanelIcons.tsx @@ -0,0 +1,42 @@ +import { SVGProps } from 'react' + +interface PanelIconProps extends Omit, 'width' | 'height'> { + size?: number | string + expanded?: boolean +} + +export const PanelLeftIcon = ({ size = 18, expanded = false, ...props }: PanelIconProps) => ( + + + {expanded ? : } + +) + +export const PanelRightIcon = ({ size = 18, expanded = false, ...props }: PanelIconProps) => ( + + + {expanded ? : } + +) diff --git a/src/renderer/src/pages/home/ChatNavbar.tsx b/src/renderer/src/pages/home/ChatNavbar.tsx index 0abba62d7b..188c8b89c6 100644 --- a/src/renderer/src/pages/home/ChatNavbar.tsx +++ b/src/renderer/src/pages/home/ChatNavbar.tsx @@ -1,5 +1,6 @@ import { Navbar } from '@renderer/components/app/Navbar' import NarrowModeIcon from '@renderer/components/Icons/NarrowModeIcon' +import { PanelLeftIcon } from '@renderer/components/Icons/PanelIcons' import { HStack } from '@renderer/components/Layout' import SearchPopup from '@renderer/components/Popups/SearchPopup' import { isLinux, isMac, isWindows } from '@renderer/config/constant' @@ -14,7 +15,7 @@ import { useAppDispatch } from '@renderer/store' import { setNarrowMode } from '@renderer/store/settings' import { Tooltip } from 'antd' import { t } from 'i18next' -import { LayoutGrid, PanelLeft, PanelRight, Search } from 'lucide-react' +import { LayoutGrid, Search } from 'lucide-react' import { FC } from 'react' import { useNavigate } from 'react-router' import styled from 'styled-components' @@ -42,20 +43,20 @@ const ChatNavbar: FC = () => { - toggleShowAssistants()}> - {showAssistants ? : } - + {!showAssistants && ( + toggleShowAssistants()}> + + + )} - {isMac && ( - - SearchPopup.show()}> - - - - )} + + SearchPopup.show()}> + + + diff --git a/src/renderer/src/pages/home/MainSidebar/MainNavbar.tsx b/src/renderer/src/pages/home/MainSidebar/MainNavbar.tsx index 68797d5805..ea467a0654 100644 --- a/src/renderer/src/pages/home/MainSidebar/MainNavbar.tsx +++ b/src/renderer/src/pages/home/MainSidebar/MainNavbar.tsx @@ -1,26 +1,24 @@ -import SearchPopup from '@renderer/components/Popups/SearchPopup' +import { PanelLeftIcon } from '@renderer/components/Icons/PanelIcons' import { isMac } from '@renderer/config/constant' +import { useShowAssistants } from '@renderer/hooks/useStore' import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService' import { Tooltip } from 'antd' import { t } from 'i18next' -import { MessageSquareDiff, Search } from 'lucide-react' +import { MessageSquareDiff } from 'lucide-react' import { FC } from 'react' import styled from 'styled-components' interface Props {} const HeaderNavbar: FC = () => { + const { showAssistants, toggleShowAssistants } = useShowAssistants() return ( -
- {!isMac && ( - - SearchPopup.show()}> - - - - )} -
+ {showAssistants && ( + toggleShowAssistants()}> + + + )} EventEmitter.emit(EVENT_NAMES.ADD_NEW_TOPIC)}> @@ -63,10 +61,4 @@ export const NavbarIcon = styled.div` } ` -const NarrowIcon = styled(NavbarIcon)` - @media (max-width: 1000px) { - display: none; - } -` - export default HeaderNavbar diff --git a/src/renderer/src/pages/home/MainSidebar/MainSidebar.tsx b/src/renderer/src/pages/home/MainSidebar/MainSidebar.tsx index 7e45693e42..b6994f8473 100644 --- a/src/renderer/src/pages/home/MainSidebar/MainSidebar.tsx +++ b/src/renderer/src/pages/home/MainSidebar/MainSidebar.tsx @@ -32,7 +32,7 @@ import { Sun, SunMoon } from 'lucide-react' -import { FC, useEffect, useState } from 'react' +import { FC, useDeferredValue, useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' import { useLocation, useNavigate } from 'react-router-dom' import styled from 'styled-components' @@ -52,6 +52,7 @@ import { SubMenu } from './MainSidebarStyles' import OpenedMinappTabs from './OpenedMinapps' +import SidebarSearch from './SidebarSearch' type Tab = 'assistants' | 'topic' @@ -73,6 +74,9 @@ const MainSidebar: FC = () => { const { openMinapp } = useMinappPopup() + const [_searchValue, setSearchValue] = useState('') + const searchValue = useDeferredValue(_searchValue) + useShortcut('toggle_show_assistants', toggleShowAssistants) useShortcut('toggle_show_topics', () => EventEmitter.emit(EVENT_NAMES.SWITCH_TOPIC_SIDEBAR)) @@ -175,6 +179,7 @@ const MainSidebar: FC = () => { }}> + setIsAppMenuExpanded(!isAppMenuExpanded)}> @@ -221,8 +226,8 @@ const MainSidebar: FC = () => { )} - {tab === 'assistants' && } - {tab === 'topic' && } + {tab === 'assistants' && } + {tab === 'topic' && } UserPopup.show()}> diff --git a/src/renderer/src/pages/home/MainSidebar/SidebarSearch.tsx b/src/renderer/src/pages/home/MainSidebar/SidebarSearch.tsx new file mode 100644 index 0000000000..4a9b28fbd3 --- /dev/null +++ b/src/renderer/src/pages/home/MainSidebar/SidebarSearch.tsx @@ -0,0 +1,106 @@ +import { Input, InputRef } from 'antd' +import { Search } from 'lucide-react' +import React, { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react' +import { useTranslation } from 'react-i18next' +import styled from 'styled-components' + +import { MainMenuItem, MainMenuItemIcon, MainMenuItemLeft, MainMenuItemText } from './MainSidebarStyles' + +interface SidebarSearchProps { + onSearch: (text: string) => void +} + +const SidebarSearch: React.FC = ({ onSearch }) => { + const { t } = useTranslation() + const [isExpanded, setIsExpanded] = useState(false) + const [searchText, setSearchText] = useState('') + const inputRef = useRef(null) + + const handleTextChange = useCallback( + (text: string) => { + setSearchText(text) + onSearch(text) + }, + [onSearch] + ) + + const handleExpand = useCallback(() => { + setIsExpanded(true) + }, []) + + const handleClear = useCallback(() => { + setSearchText('') + onSearch('') + }, [onSearch]) + + const handleCollapse = useCallback(() => { + setSearchText('') + setIsExpanded(false) + onSearch('') + }, [onSearch]) + + const handleInputKeyDown = useCallback( + (e: React.KeyboardEvent) => { + if (e.key === 'Escape') { + handleCollapse() + } + }, + [handleCollapse] + ) + + useEffect(() => { + if (isExpanded && inputRef.current) { + inputRef.current.focus() + } + }, [isExpanded]) + + const renderInputBox = useMemo(() => { + return ( + handleTextChange(e.target.value)} + onKeyDown={handleInputKeyDown} + onBlur={(e) => { + // 如果输入框失焦且没有搜索内容,则收起 + if (!e.target.value.trim()) { + handleCollapse() + } + }} + onClear={handleClear} + allowClear + style={{ + paddingTop: 4 + }} + prefix={ + + + + } + spellCheck={false} + /> + ) + }, [handleClear, handleCollapse, handleInputKeyDown, handleTextChange, searchText, t]) + + const renderMenuItem = useMemo(() => { + return ( + + + + + + {t('chat.assistant.search.placeholder')} + + + ) + }, [handleExpand, t]) + + return {isExpanded ? renderInputBox : renderMenuItem} +} + +const SearchBarWrapper = styled.div` + height: 2.2rem; +` + +export default memo(SidebarSearch) diff --git a/src/renderer/src/pages/home/Tabs/AssistantsTab.tsx b/src/renderer/src/pages/home/Tabs/AssistantsTab.tsx index 9c7aa42f06..28332ddd08 100644 --- a/src/renderer/src/pages/home/Tabs/AssistantsTab.tsx +++ b/src/renderer/src/pages/home/Tabs/AssistantsTab.tsx @@ -9,14 +9,19 @@ import { useAssistantsTabSortType } from '@renderer/hooks/useStore' import { useTags } from '@renderer/hooks/useTags' import { Assistant, AssistantsSortType } from '@renderer/types' import { uuid } from '@renderer/utils' +import { includeKeywords } from '@renderer/utils/search' import { Tooltip } from 'antd' -import { FC, useCallback, useRef, useState } from 'react' +import { FC, useCallback, useMemo, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import styled from 'styled-components' import AssistantItem from './components/AssistantItem' -const Assistants: FC = () => { +interface AssistantsTabProps { + searchValue?: string +} + +const Assistants: FC = ({ searchValue }) => { const { activeAssistant, setActiveAssistant } = useChat() const { assistants, removeAssistant, addAssistant, updateAssistants } = useAssistants() const [dragging, setDragging] = useState(false) @@ -27,6 +32,27 @@ const Assistants: FC = () => { const containerRef = useRef(null) const { defaultAssistant } = useDefaultAssistant() + // 过滤助手 - 根据名称搜索 + const filteredAssistants = useMemo(() => { + if (!searchValue?.trim()) { + return assistants + } + return assistants.filter((assistant) => includeKeywords(assistant.name || '', searchValue)) + }, [assistants, searchValue]) + + // 过滤分组助手 - 根据名称搜索 + const filteredGroupedAssistants = useMemo(() => { + if (!searchValue?.trim()) { + return getGroupedAssistants + } + return getGroupedAssistants + .map((group) => ({ + ...group, + assistants: group.assistants.filter((assistant) => includeKeywords(assistant.name || '', searchValue)) + })) + .filter((group) => group.assistants.length > 0) + }, [getGroupedAssistants, searchValue]) + const onCreateAssistant = async () => { const assistant = await AddAssistantPopup.show() assistant && setActiveAssistant(assistant) @@ -166,7 +192,7 @@ const Assistants: FC = () => { ({ ..._, disabled: _.tag === t('assistants.tags.untagged') }))} + list={filteredGroupedAssistants.map((_) => ({ ..._, disabled: _.tag === t('assistants.tags.untagged') }))} onUpdate={() => {}} onDragEnd={handleGroupDragEnd}> {(group) => ( @@ -234,7 +260,7 @@ const Assistants: FC = () => { return ( setDragging(true)} diff --git a/src/renderer/src/pages/home/Tabs/TopicsTab.tsx b/src/renderer/src/pages/home/Tabs/TopicsTab.tsx index 51bdd59e58..07e51b0c2f 100644 --- a/src/renderer/src/pages/home/Tabs/TopicsTab.tsx +++ b/src/renderer/src/pages/home/Tabs/TopicsTab.tsx @@ -36,6 +36,7 @@ import { topicToMarkdown } from '@renderer/utils/export' import { hasTopicPendingRequests } from '@renderer/utils/queue' +import { includeKeywords } from '@renderer/utils/search' import { Dropdown, MenuProps, Tooltip } from 'antd' import { ItemType, MenuItemType } from 'antd/es/menu/interface' import dayjs from 'dayjs' @@ -46,10 +47,11 @@ import { useSelector } from 'react-redux' import styled from 'styled-components' interface TopicsTabProps { + searchValue?: string style?: React.CSSProperties } -const Topics: FC = ({ style }) => { +const Topics: FC = ({ searchValue, style }) => { const { activeAssistant, activeTopic, setActiveTopic } = useChat() const { assistants } = useAssistants() const { assistant, removeTopic, moveTopic, updateTopic, updateTopics } = useAssistant(activeAssistant.id) @@ -429,10 +431,18 @@ const Topics: FC = ({ style }) => { return topics }, [topics, pinTopicsToTop]) + // 过滤话题 - 根据名称搜索 + const filteredTopics = useMemo(() => { + if (!searchValue?.trim()) { + return sortedTopics + } + return sortedTopics.filter((topic) => includeKeywords(topic.name || '', searchValue)) + }, [sortedTopics, searchValue]) + return ( - + {(topic) => { const isActive = topic.id === activeTopic?.id const topicName = topic.name.replace('`', '') diff --git a/src/renderer/src/utils/search.ts b/src/renderer/src/utils/search.ts new file mode 100644 index 0000000000..b80ace1ceb --- /dev/null +++ b/src/renderer/src/utils/search.ts @@ -0,0 +1,20 @@ +/** + * 判断一个字符串是否包含由另一个字符串表示的 keywords + * 将 keywords 按空白字符分割成多个关键词,检查目标字符串是否包含所有关键词 + * - 大小写不敏感 + * - 支持的分隔符:空格、制表符、换行符等各种空白字符 + * + * @param target 被搜索的字符串 + * @param search 搜索词(用空白字符分隔) + */ +export function includeKeywords(target: string, search: string): boolean { + if (!search?.trim()) return true + if (!target) return false + + const targetLower = target.toLowerCase() + const searchLower = search.toLowerCase() + + const keywords = searchLower.split(/\s+/).filter((keyword) => keyword.trim()) + + return keywords.every((keyword) => targetLower.includes(keyword)) +}