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
This commit is contained in:
one 2025-06-16 12:35:24 +08:00 committed by GitHub
parent 05727c637f
commit 202504fd17
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 240 additions and 38 deletions

View File

@ -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;

View File

@ -0,0 +1,42 @@
import { SVGProps } from 'react'
interface PanelIconProps extends Omit<SVGProps<SVGSVGElement>, 'width' | 'height'> {
size?: number | string
expanded?: boolean
}
export const PanelLeftIcon = ({ size = 18, expanded = false, ...props }: PanelIconProps) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width={size}
height={size}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
className="lucide lucide-panel-left-icon lucide-panel-left"
{...props}>
<rect width="18" height="18" x="3" y="3" rx="2" />
{expanded ? <path d="M10 7v10" strokeWidth={4} /> : <path d="M9 6v12" strokeWidth={2} />}
</svg>
)
export const PanelRightIcon = ({ size = 18, expanded = false, ...props }: PanelIconProps) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width={size}
height={size}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
className="lucide lucide-panel-right-icon lucide-panel-right"
{...props}>
<rect width="18" height="18" x="3" y="3" rx="2" />
{expanded ? <path d="M14 7v10" strokeWidth={4} /> : <path d="M15 6v12" strokeWidth={2} />}
</svg>
)

View File

@ -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 = () => {
<Navbar className="home-navbar">
<NavbarContainer $isFullscreen={isFullscreen} $showSidebar={showAssistants} className="home-navbar-right">
<HStack alignItems="center" gap={8}>
<NavbarIcon onClick={() => toggleShowAssistants()}>
{showAssistants ? <PanelLeft size={18} /> : <PanelRight size={18} />}
</NavbarIcon>
{!showAssistants && (
<NavbarIcon onClick={() => toggleShowAssistants()}>
<PanelLeftIcon size={18} expanded={false} />
</NavbarIcon>
)}
<SelectModelButton assistant={assistant} />
</HStack>
<HStack alignItems="center" gap={8}>
<UpdateAppButton />
{isMac && (
<Tooltip title={t('chat.assistant.search.placeholder')} mouseEnterDelay={0.8}>
<NarrowIcon onClick={() => SearchPopup.show()}>
<Search size={18} />
</NarrowIcon>
</Tooltip>
)}
<Tooltip title={t('history.title')} mouseEnterDelay={0.8}>
<NarrowIcon onClick={() => SearchPopup.show()}>
<Search size={18} />
</NarrowIcon>
</Tooltip>
<Tooltip title={t('navbar.expand')} mouseEnterDelay={0.8}>
<NarrowIcon onClick={handleNarrowModeToggle}>
<NarrowModeIcon isNarrowMode={narrowMode} />

View File

@ -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<Props> = () => {
const { showAssistants, toggleShowAssistants } = useShowAssistants()
return (
<Container>
<div>
{!isMac && (
<Tooltip title={t('chat.assistant.search.placeholder')} mouseEnterDelay={0.8}>
<NarrowIcon onClick={() => SearchPopup.show()}>
<Search size={18} />
</NarrowIcon>
</Tooltip>
)}
</div>
{showAssistants && (
<NavbarIcon onClick={() => toggleShowAssistants()}>
<PanelLeftIcon size={18} expanded={true} />
</NavbarIcon>
)}
<Tooltip title={t('settings.shortcuts.new_topic')} mouseEnterDelay={0.8}>
<NavbarIcon onClick={() => EventEmitter.emit(EVENT_NAMES.ADD_NEW_TOPIC)}>
<MessageSquareDiff size={18} />
@ -63,10 +61,4 @@ export const NavbarIcon = styled.div`
}
`
const NarrowIcon = styled(NavbarIcon)`
@media (max-width: 1000px) {
display: none;
}
`
export default HeaderNavbar

View File

@ -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 = () => {
}}>
<MainNavbar />
<MainMenu>
<SidebarSearch onSearch={setSearchValue} />
<MainMenuItem active={isAppMenuExpanded} onClick={() => setIsAppMenuExpanded(!isAppMenuExpanded)}>
<MainMenuItemLeft>
<MainMenuItemIcon>
@ -221,8 +226,8 @@ const MainSidebar: FC = () => {
</AssistantContainer>
)}
<MainContainer>
{tab === 'assistants' && <AssistantsTab />}
{tab === 'topic' && <TopicsTab style={{ paddingTop: 4 }} />}
{tab === 'assistants' && <AssistantsTab searchValue={searchValue} />}
{tab === 'topic' && <TopicsTab searchValue={searchValue} style={{ paddingTop: 4 }} />}
</MainContainer>
<UserMenu>
<UserMenuLeft onClick={() => UserPopup.show()}>

View File

@ -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<SidebarSearchProps> = ({ onSearch }) => {
const { t } = useTranslation()
const [isExpanded, setIsExpanded] = useState(false)
const [searchText, setSearchText] = useState('')
const inputRef = useRef<InputRef>(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 (
<Input
ref={inputRef}
value={searchText}
placeholder={t('chat.assistant.search.placeholder')}
onChange={(e) => handleTextChange(e.target.value)}
onKeyDown={handleInputKeyDown}
onBlur={(e) => {
// 如果输入框失焦且没有搜索内容,则收起
if (!e.target.value.trim()) {
handleCollapse()
}
}}
onClear={handleClear}
allowClear
style={{
paddingTop: 4
}}
prefix={
<MainMenuItemIcon style={{ margin: '0 6px 0 -2px' }}>
<Search size={18} className="icon" />
</MainMenuItemIcon>
}
spellCheck={false}
/>
)
}, [handleClear, handleCollapse, handleInputKeyDown, handleTextChange, searchText, t])
const renderMenuItem = useMemo(() => {
return (
<MainMenuItem onClick={handleExpand} style={{ cursor: 'pointer' }}>
<MainMenuItemLeft>
<MainMenuItemIcon>
<Search size={18} className="icon" />
</MainMenuItemIcon>
<MainMenuItemText>{t('chat.assistant.search.placeholder')}</MainMenuItemText>
</MainMenuItemLeft>
</MainMenuItem>
)
}, [handleExpand, t])
return <SearchBarWrapper>{isExpanded ? renderInputBox : renderMenuItem}</SearchBarWrapper>
}
const SearchBarWrapper = styled.div`
height: 2.2rem;
`
export default memo(SidebarSearch)

View File

@ -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<AssistantsTabProps> = ({ 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<HTMLDivElement>(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 = () => {
<Container className="assistants-tab" ref={containerRef}>
<DragableList
droppableProps={{ type: 'TAG' }}
list={getGroupedAssistants.map((_) => ({ ..._, 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 (
<Container className="assistants-tab" ref={containerRef}>
<DragableList
list={assistants}
list={filteredAssistants}
onUpdate={updateAssistants}
style={{ paddingBottom: dragging ? '34px' : 0 }}
onDragStart={() => setDragging(true)}

View File

@ -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<TopicsTabProps> = ({ style }) => {
const Topics: FC<TopicsTabProps> = ({ 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<TopicsTabProps> = ({ style }) => {
return topics
}, [topics, pinTopicsToTop])
// 过滤话题 - 根据名称搜索
const filteredTopics = useMemo(() => {
if (!searchValue?.trim()) {
return sortedTopics
}
return sortedTopics.filter((topic) => includeKeywords(topic.name || '', searchValue))
}, [sortedTopics, searchValue])
return (
<Dropdown menu={{ items: getTopicMenuItems }} trigger={['contextMenu']}>
<Container className={`topics-tab ${topicPosition === 'right' ? 'right' : ''}`} style={style}>
<DragableList list={sortedTopics} onUpdate={updateTopics}>
<DragableList list={filteredTopics} onUpdate={updateTopics}>
{(topic) => {
const isActive = topic.id === activeTopic?.id
const topicName = topic.name.replace('`', '')

View File

@ -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))
}