mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2026-01-08 06:19:05 +08:00
feature: unified assistant tab (#10590)
* feature: unified assistant tab * refactor(TagGroup): make TagsContainer component internal by removing export * refactor(components): migrate styled-components to cn utility classes Replace styled-components with cn utility classes from @heroui/react for better maintainability and performance * refactor(AssistantsTab): split AssistantsTab into smaller hooks and components * fix: click agent item should jump to topic tab * feat: add AddButton component and refactor usage across tabs - Introduced a new AddButton component for consistent UI across different tabs. - Replaced existing button implementations with AddButton in Sessions, Topics, and UnifiedAddButton components. - Removed unnecessary margin from AssistantsTab's container for improved layout. --------- Co-authored-by: icarus <eurfelux@gmail.com> Co-authored-by: kangfenmao <kangfenmao@qq.com>
This commit is contained in:
parent
5f469a71f3
commit
9473ddc762
@ -1,10 +1,26 @@
|
|||||||
|
import { Alert, Spinner } from '@heroui/react'
|
||||||
import Scrollbar from '@renderer/components/Scrollbar'
|
import Scrollbar from '@renderer/components/Scrollbar'
|
||||||
import { Assistant } from '@renderer/types'
|
import { useAgents } from '@renderer/hooks/agents/useAgents'
|
||||||
import { FC, useRef } from 'react'
|
import { useAssistants } from '@renderer/hooks/useAssistant'
|
||||||
|
import { useAssistantPresets } from '@renderer/hooks/useAssistantPresets'
|
||||||
|
import { useRuntime } from '@renderer/hooks/useRuntime'
|
||||||
|
import { useSettings } from '@renderer/hooks/useSettings'
|
||||||
|
import { useAssistantsTabSortType } from '@renderer/hooks/useStore'
|
||||||
|
import { useTags } from '@renderer/hooks/useTags'
|
||||||
|
import { useAppDispatch } from '@renderer/store'
|
||||||
|
import { addIknowAction } from '@renderer/store/runtime'
|
||||||
|
import { Assistant, AssistantsSortType } from '@renderer/types'
|
||||||
|
import { FC, useCallback, useEffect, useRef, useState } from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
|
|
||||||
import { AgentSection } from './components/AgentSection'
|
import UnifiedAddButton from './components/UnifiedAddButton'
|
||||||
import Assistants from './components/Assistants'
|
import { UnifiedList } from './components/UnifiedList'
|
||||||
|
import { UnifiedTagGroups } from './components/UnifiedTagGroups'
|
||||||
|
import { useActiveAgent } from './hooks/useActiveAgent'
|
||||||
|
import { useUnifiedGrouping } from './hooks/useUnifiedGrouping'
|
||||||
|
import { useUnifiedItems } from './hooks/useUnifiedItems'
|
||||||
|
import { useUnifiedSorting } from './hooks/useUnifiedSorting'
|
||||||
|
|
||||||
interface AssistantsTabProps {
|
interface AssistantsTabProps {
|
||||||
activeAssistant: Assistant
|
activeAssistant: Assistant
|
||||||
@ -13,12 +29,143 @@ interface AssistantsTabProps {
|
|||||||
onCreateDefaultAssistant: () => void
|
onCreateDefaultAssistant: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const ALERT_KEY = 'enable_api_server_to_use_agent'
|
||||||
|
|
||||||
const AssistantsTab: FC<AssistantsTabProps> = (props) => {
|
const AssistantsTab: FC<AssistantsTabProps> = (props) => {
|
||||||
|
const { activeAssistant, setActiveAssistant, onCreateAssistant, onCreateDefaultAssistant } = props
|
||||||
const containerRef = useRef<HTMLDivElement>(null)
|
const containerRef = useRef<HTMLDivElement>(null)
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const { apiServer } = useSettings()
|
||||||
|
const { iknow, chat } = useRuntime()
|
||||||
|
const dispatch = useAppDispatch()
|
||||||
|
|
||||||
|
// Agent related hooks
|
||||||
|
const { agents, deleteAgent, isLoading: agentsLoading, error: agentsError } = useAgents()
|
||||||
|
const { activeAgentId } = chat
|
||||||
|
const { setActiveAgentId } = useActiveAgent()
|
||||||
|
|
||||||
|
// Assistant related hooks
|
||||||
|
const { assistants, removeAssistant, copyAssistant, updateAssistants } = useAssistants()
|
||||||
|
const { addAssistantPreset } = useAssistantPresets()
|
||||||
|
const { collapsedTags, toggleTagCollapse } = useTags()
|
||||||
|
const { assistantsTabSortType = 'list', setAssistantsTabSortType } = useAssistantsTabSortType()
|
||||||
|
const [dragging, setDragging] = useState(false)
|
||||||
|
|
||||||
|
// Unified items management
|
||||||
|
const { unifiedItems, handleUnifiedListReorder } = useUnifiedItems({
|
||||||
|
agents,
|
||||||
|
assistants,
|
||||||
|
apiServerEnabled: apiServer.enabled,
|
||||||
|
agentsLoading,
|
||||||
|
agentsError,
|
||||||
|
updateAssistants
|
||||||
|
})
|
||||||
|
|
||||||
|
// Sorting
|
||||||
|
const { sortByPinyinAsc, sortByPinyinDesc } = useUnifiedSorting({
|
||||||
|
unifiedItems,
|
||||||
|
updateAssistants
|
||||||
|
})
|
||||||
|
|
||||||
|
// Grouping
|
||||||
|
const { groupedUnifiedItems, handleUnifiedGroupReorder } = useUnifiedGrouping({
|
||||||
|
unifiedItems,
|
||||||
|
assistants,
|
||||||
|
agents,
|
||||||
|
apiServerEnabled: apiServer.enabled,
|
||||||
|
agentsLoading,
|
||||||
|
agentsError,
|
||||||
|
updateAssistants
|
||||||
|
})
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!agentsLoading && agents.length > 0 && !activeAgentId && apiServer.enabled) {
|
||||||
|
setActiveAgentId(agents[0].id)
|
||||||
|
}
|
||||||
|
}, [agentsLoading, agents, activeAgentId, setActiveAgentId, apiServer.enabled])
|
||||||
|
|
||||||
|
const onDeleteAssistant = useCallback(
|
||||||
|
(assistant: Assistant) => {
|
||||||
|
const remaining = assistants.filter((a) => a.id !== assistant.id)
|
||||||
|
if (assistant.id === activeAssistant?.id) {
|
||||||
|
const newActive = remaining[remaining.length - 1]
|
||||||
|
newActive ? setActiveAssistant(newActive) : onCreateDefaultAssistant()
|
||||||
|
}
|
||||||
|
removeAssistant(assistant.id)
|
||||||
|
},
|
||||||
|
[activeAssistant, assistants, removeAssistant, setActiveAssistant, onCreateDefaultAssistant]
|
||||||
|
)
|
||||||
|
|
||||||
|
const handleSortByChange = useCallback(
|
||||||
|
(sortType: AssistantsSortType) => {
|
||||||
|
setAssistantsTabSortType(sortType)
|
||||||
|
},
|
||||||
|
[setAssistantsTabSortType]
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container className="assistants-tab" ref={containerRef}>
|
<Container className="assistants-tab" ref={containerRef}>
|
||||||
<AgentSection />
|
{!apiServer.enabled && !iknow[ALERT_KEY] && (
|
||||||
<Assistants {...props} />
|
<Alert
|
||||||
|
color="warning"
|
||||||
|
title={t('agent.warning.enable_server')}
|
||||||
|
isClosable
|
||||||
|
onClose={() => {
|
||||||
|
dispatch(addIknowAction(ALERT_KEY))
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<UnifiedAddButton onCreateAssistant={onCreateAssistant} />
|
||||||
|
|
||||||
|
{agentsLoading && <Spinner />}
|
||||||
|
{apiServer.enabled && agentsError && <Alert color="danger" title={t('agent.list.error.failed')} />}
|
||||||
|
|
||||||
|
{assistantsTabSortType === 'tags' ? (
|
||||||
|
<UnifiedTagGroups
|
||||||
|
groupedItems={groupedUnifiedItems}
|
||||||
|
activeAssistantId={activeAssistant.id}
|
||||||
|
activeAgentId={activeAgentId}
|
||||||
|
sortBy={assistantsTabSortType}
|
||||||
|
collapsedTags={collapsedTags}
|
||||||
|
onGroupReorder={handleUnifiedGroupReorder}
|
||||||
|
onDragStart={() => setDragging(true)}
|
||||||
|
onDragEnd={() => setDragging(false)}
|
||||||
|
onToggleTagCollapse={toggleTagCollapse}
|
||||||
|
onAssistantSwitch={setActiveAssistant}
|
||||||
|
onAssistantDelete={onDeleteAssistant}
|
||||||
|
onAgentDelete={deleteAgent}
|
||||||
|
onAgentPress={setActiveAgentId}
|
||||||
|
addPreset={addAssistantPreset}
|
||||||
|
copyAssistant={copyAssistant}
|
||||||
|
onCreateDefaultAssistant={onCreateDefaultAssistant}
|
||||||
|
handleSortByChange={handleSortByChange}
|
||||||
|
sortByPinyinAsc={sortByPinyinAsc}
|
||||||
|
sortByPinyinDesc={sortByPinyinDesc}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<UnifiedList
|
||||||
|
items={unifiedItems}
|
||||||
|
activeAssistantId={activeAssistant.id}
|
||||||
|
activeAgentId={activeAgentId}
|
||||||
|
sortBy={assistantsTabSortType}
|
||||||
|
onReorder={handleUnifiedListReorder}
|
||||||
|
onDragStart={() => setDragging(true)}
|
||||||
|
onDragEnd={() => setDragging(false)}
|
||||||
|
onAssistantSwitch={setActiveAssistant}
|
||||||
|
onAssistantDelete={onDeleteAssistant}
|
||||||
|
onAgentDelete={deleteAgent}
|
||||||
|
onAgentPress={setActiveAgentId}
|
||||||
|
addPreset={addAssistantPreset}
|
||||||
|
copyAssistant={copyAssistant}
|
||||||
|
onCreateDefaultAssistant={onCreateDefaultAssistant}
|
||||||
|
handleSortByChange={handleSortByChange}
|
||||||
|
sortByPinyinAsc={sortByPinyinAsc}
|
||||||
|
sortByPinyinDesc={sortByPinyinDesc}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!dragging && <div style={{ minHeight: 10 }}></div>}
|
||||||
</Container>
|
</Container>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -27,7 +174,6 @@ const Container = styled(Scrollbar)`
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
margin-top: 3px;
|
|
||||||
`
|
`
|
||||||
|
|
||||||
export default AssistantsTab
|
export default AssistantsTab
|
||||||
|
|||||||
24
src/renderer/src/pages/home/Tabs/components/AddButton.tsx
Normal file
24
src/renderer/src/pages/home/Tabs/components/AddButton.tsx
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import { Button, ButtonProps, cn } from '@heroui/react'
|
||||||
|
import { PlusIcon } from 'lucide-react'
|
||||||
|
import { FC } from 'react'
|
||||||
|
|
||||||
|
interface Props extends ButtonProps {
|
||||||
|
children: React.ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
const AddButton: FC<Props> = ({ children, className, ...props }) => {
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
{...props}
|
||||||
|
onPress={props.onPress}
|
||||||
|
className={cn(
|
||||||
|
'h-9 w-[calc(var(--assistants-width)-20px)] justify-start rounded-full bg-transparent px-3 text-[13px] text-[var(--color-text-2)] hover:bg-[var(--color-list-item)]',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
startContent={<PlusIcon size={16} className="shrink-0" />}>
|
||||||
|
{children}
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AddButton
|
||||||
@ -1,11 +1,14 @@
|
|||||||
import { Button, Chip, cn } from '@heroui/react'
|
import { cn } from '@heroui/react'
|
||||||
import { DeleteIcon, EditIcon } from '@renderer/components/Icons'
|
import { DeleteIcon, EditIcon } from '@renderer/components/Icons'
|
||||||
import { useSessions } from '@renderer/hooks/agents/useSessions'
|
import { useSessions } from '@renderer/hooks/agents/useSessions'
|
||||||
|
import { useSettings } from '@renderer/hooks/useSettings'
|
||||||
import AgentSettingsPopup from '@renderer/pages/settings/AgentSettings/AgentSettingsPopup'
|
import AgentSettingsPopup from '@renderer/pages/settings/AgentSettings/AgentSettingsPopup'
|
||||||
import { AgentLabel } from '@renderer/pages/settings/AgentSettings/shared'
|
import { AgentLabel } from '@renderer/pages/settings/AgentSettings/shared'
|
||||||
|
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
|
||||||
import { AgentEntity } from '@renderer/types'
|
import { AgentEntity } from '@renderer/types'
|
||||||
import { ContextMenu, ContextMenuContent, ContextMenuItem, ContextMenuTrigger } from '@renderer/ui/context-menu'
|
import { ContextMenu, ContextMenuContent, ContextMenuItem, ContextMenuTrigger } from '@renderer/ui/context-menu'
|
||||||
import { FC, memo } from 'react'
|
import { Bot } from 'lucide-react'
|
||||||
|
import { FC, memo, useCallback } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
// const logger = loggerService.withContext('AgentItem')
|
// const logger = loggerService.withContext('AgentItem')
|
||||||
@ -20,81 +23,107 @@ interface AgentItemProps {
|
|||||||
const AgentItem: FC<AgentItemProps> = ({ agent, isActive, onDelete, onPress }) => {
|
const AgentItem: FC<AgentItemProps> = ({ agent, isActive, onDelete, onPress }) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { sessions } = useSessions(agent.id)
|
const { sessions } = useSessions(agent.id)
|
||||||
|
const { clickAssistantToShowTopic, topicPosition } = useSettings()
|
||||||
|
|
||||||
|
const handlePress = useCallback(() => {
|
||||||
|
// Show session sidebar if setting is enabled (reusing the assistant setting for consistency)
|
||||||
|
if (clickAssistantToShowTopic) {
|
||||||
|
if (topicPosition === 'left') {
|
||||||
|
EventEmitter.emit(EVENT_NAMES.SWITCH_TOPIC_SIDEBAR)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onPress()
|
||||||
|
}, [clickAssistantToShowTopic, topicPosition, onPress])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<ContextMenu modal={false}>
|
||||||
<ContextMenu modal={false}>
|
<ContextMenuTrigger>
|
||||||
<ContextMenuTrigger>
|
<Container onClick={handlePress} isActive={isActive}>
|
||||||
<ButtonContainer onPress={onPress} className={isActive ? 'active' : ''}>
|
<AssistantNameRow className="name" title={agent.name ?? agent.id}>
|
||||||
<AssistantNameRow className="name flex w-full justify-between" title={agent.name ?? agent.id}>
|
<AgentNameWrapper>
|
||||||
<AgentLabel agent={agent} />
|
<AgentLabel agent={agent} />
|
||||||
{isActive && (
|
</AgentNameWrapper>
|
||||||
<Chip
|
</AssistantNameRow>
|
||||||
variant="bordered"
|
<MenuButton>
|
||||||
size="sm"
|
{isActive ? <SessionCount>{sessions.length}</SessionCount> : <Bot size={12} className="text-primary" />}
|
||||||
radius="full"
|
</MenuButton>
|
||||||
className="aspect-square h-5 w-5 items-center justify-center border-[0.5px] bg-background text-[10px]">
|
</Container>
|
||||||
{sessions.length}
|
</ContextMenuTrigger>
|
||||||
</Chip>
|
<ContextMenuContent>
|
||||||
)}
|
<ContextMenuItem
|
||||||
</AssistantNameRow>
|
key="edit"
|
||||||
</ButtonContainer>
|
onClick={async () => {
|
||||||
</ContextMenuTrigger>
|
// onOpen()
|
||||||
<ContextMenuContent>
|
await AgentSettingsPopup.show({
|
||||||
<ContextMenuItem
|
agentId: agent.id
|
||||||
key="edit"
|
})
|
||||||
onClick={async () => {
|
}}>
|
||||||
// onOpen()
|
<EditIcon size={14} />
|
||||||
await AgentSettingsPopup.show({
|
{t('common.edit')}
|
||||||
agentId: agent.id
|
</ContextMenuItem>
|
||||||
})
|
<ContextMenuItem
|
||||||
}}>
|
key="delete"
|
||||||
<EditIcon size={14} />
|
className="text-danger"
|
||||||
{t('common.edit')}
|
onClick={() => {
|
||||||
</ContextMenuItem>
|
window.modal.confirm({
|
||||||
<ContextMenuItem
|
title: t('agent.delete.title'),
|
||||||
key="delete"
|
content: t('agent.delete.content'),
|
||||||
className="text-danger"
|
centered: true,
|
||||||
onClick={() => {
|
okButtonProps: { danger: true },
|
||||||
window.modal.confirm({
|
onOk: () => onDelete(agent)
|
||||||
title: t('agent.delete.title'),
|
})
|
||||||
content: t('agent.delete.content'),
|
}}>
|
||||||
centered: true,
|
<DeleteIcon size={14} className="lucide-custom text-danger" />
|
||||||
okButtonProps: { danger: true },
|
<span className="text-danger">{t('common.delete')}</span>
|
||||||
onOk: () => onDelete(agent)
|
</ContextMenuItem>
|
||||||
})
|
</ContextMenuContent>
|
||||||
}}>
|
</ContextMenu>
|
||||||
<DeleteIcon size={14} className="lucide-custom text-danger" />
|
|
||||||
<span className="text-danger">{t('common.delete')}</span>
|
|
||||||
</ContextMenuItem>
|
|
||||||
</ContextMenuContent>
|
|
||||||
</ContextMenu>
|
|
||||||
{/* <AgentModal isOpen={isOpen} onClose={onClose} agent={agent} /> */}
|
|
||||||
</>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const ButtonContainer: React.FC<React.ComponentProps<typeof Button>> = ({ className, children, ...props }) => (
|
export const Container: React.FC<{ isActive?: boolean } & React.HTMLAttributes<HTMLDivElement>> = ({
|
||||||
<Button
|
className,
|
||||||
{...props}
|
isActive,
|
||||||
|
...props
|
||||||
|
}) => (
|
||||||
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'relative mb-2 flex h-[37px] flex-row justify-between p-2.5',
|
'relative flex h-[37px] w-[calc(var(--assistants-width)-20px)] cursor-pointer flex-row justify-between rounded-[var(--list-item-border-radius)] border border-transparent px-2 hover:bg-[var(--color-list-item-hover)]',
|
||||||
'rounded-[var(--list-item-border-radius)]',
|
isActive && 'bg-[var(--color-list-item)] shadow-[0_1px_2px_0_rgba(0,0,0,0.05)]',
|
||||||
'border-[0.5px] border-transparent',
|
|
||||||
'w-[calc(var(--assistants-width)_-_20px)]',
|
|
||||||
'bg-transparent hover:bg-[var(--color-list-item)] hover:shadow-sm',
|
|
||||||
'cursor-pointer',
|
|
||||||
className?.includes('active') && 'bg-[var(--color-list-item)] shadow-sm',
|
|
||||||
className
|
className
|
||||||
)}>
|
)}
|
||||||
{children}
|
{...props}
|
||||||
</Button>
|
/>
|
||||||
)
|
)
|
||||||
|
|
||||||
const AssistantNameRow: React.FC<React.HTMLAttributes<HTMLDivElement>> = ({ className, ...props }) => (
|
export const AssistantNameRow: React.FC<React.HTMLAttributes<HTMLDivElement>> = ({ className, ...props }) => (
|
||||||
<div
|
<div
|
||||||
|
className={cn('flex min-w-0 flex-1 flex-row items-center gap-2 text-[13px] text-[var(--color-text)]', className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
|
||||||
|
export const AgentNameWrapper: React.FC<React.HTMLAttributes<HTMLDivElement>> = ({ className, ...props }) => (
|
||||||
|
<div className={cn('min-w-0 flex-1 overflow-hidden text-ellipsis whitespace-nowrap', className)} {...props} />
|
||||||
|
)
|
||||||
|
|
||||||
|
export const MenuButton: React.FC<React.HTMLAttributes<HTMLDivElement>> = ({ className, ...props }) => (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'absolute top-[6px] right-[9px] flex h-[22px] min-h-[22px] w-[22px] flex-row items-center justify-center rounded-full border border-[var(--color-border)] bg-[var(--color-background)] px-[5px]',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
|
||||||
|
export const SessionCount: React.FC<React.HTMLAttributes<HTMLDivElement>> = ({ className, ...props }) => (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'flex flex-row items-center justify-center rounded-full text-[10px] text-[var(--color-text)]',
|
||||||
|
className
|
||||||
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
className={cn('text-[13px] text-[var(--color-text)]', 'flex flex-row items-center gap-2', className)}
|
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@ -1,39 +0,0 @@
|
|||||||
import { Alert } from '@heroui/react'
|
|
||||||
import { useRuntime } from '@renderer/hooks/useRuntime'
|
|
||||||
import { useSettings } from '@renderer/hooks/useSettings'
|
|
||||||
import { useAppDispatch } from '@renderer/store'
|
|
||||||
import { addIknowAction } from '@renderer/store/runtime'
|
|
||||||
import { useTranslation } from 'react-i18next'
|
|
||||||
|
|
||||||
import { Agents } from './Agents'
|
|
||||||
import { SectionName } from './SectionName'
|
|
||||||
|
|
||||||
const ALERT_KEY = 'enable_api_server_to_use_agent'
|
|
||||||
|
|
||||||
export const AgentSection = () => {
|
|
||||||
const { t } = useTranslation()
|
|
||||||
const { apiServer } = useSettings()
|
|
||||||
const { iknow } = useRuntime()
|
|
||||||
const dispatch = useAppDispatch()
|
|
||||||
|
|
||||||
if (!apiServer.enabled) {
|
|
||||||
if (iknow[ALERT_KEY]) return null
|
|
||||||
return (
|
|
||||||
<Alert
|
|
||||||
color="warning"
|
|
||||||
title={t('agent.warning.enable_server')}
|
|
||||||
isClosable
|
|
||||||
onClose={() => {
|
|
||||||
dispatch(addIknowAction(ALERT_KEY))
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="agents-tab mb-2 h-full w-full">
|
|
||||||
<SectionName name={t('common.agent_other')} />
|
|
||||||
<Agents />
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@ -1,3 +1,4 @@
|
|||||||
|
import { cn } from '@heroui/react'
|
||||||
import ModelAvatar from '@renderer/components/Avatar/ModelAvatar'
|
import ModelAvatar from '@renderer/components/Avatar/ModelAvatar'
|
||||||
import EmojiIcon from '@renderer/components/EmojiIcon'
|
import EmojiIcon from '@renderer/components/EmojiIcon'
|
||||||
import { CopyIcon, DeleteIcon, EditIcon } from '@renderer/components/Icons'
|
import { CopyIcon, DeleteIcon, EditIcon } from '@renderer/components/Icons'
|
||||||
@ -28,9 +29,8 @@ import {
|
|||||||
Tag,
|
Tag,
|
||||||
Tags
|
Tags
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
import { FC, memo, useCallback, useEffect, useMemo, useState } from 'react'
|
import { FC, memo, PropsWithChildren, useCallback, useEffect, useMemo, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import styled from 'styled-components'
|
|
||||||
import * as tinyPinyin from 'tiny-pinyin'
|
import * as tinyPinyin from 'tiny-pinyin'
|
||||||
|
|
||||||
import AssistantTagsPopup from './AssistantTagsPopup'
|
import AssistantTagsPopup from './AssistantTagsPopup'
|
||||||
@ -46,6 +46,8 @@ interface AssistantItemProps {
|
|||||||
copyAssistant: (assistant: Assistant) => void
|
copyAssistant: (assistant: Assistant) => void
|
||||||
onTagClick?: (tag: string) => void
|
onTagClick?: (tag: string) => void
|
||||||
handleSortByChange?: (sortType: AssistantsSortType) => void
|
handleSortByChange?: (sortType: AssistantsSortType) => void
|
||||||
|
sortByPinyinAsc?: () => void
|
||||||
|
sortByPinyinDesc?: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const AssistantItem: FC<AssistantItemProps> = ({
|
const AssistantItem: FC<AssistantItemProps> = ({
|
||||||
@ -56,7 +58,9 @@ const AssistantItem: FC<AssistantItemProps> = ({
|
|||||||
onDelete,
|
onDelete,
|
||||||
addPreset,
|
addPreset,
|
||||||
copyAssistant,
|
copyAssistant,
|
||||||
handleSortByChange
|
handleSortByChange,
|
||||||
|
sortByPinyinAsc: externalSortByPinyinAsc,
|
||||||
|
sortByPinyinDesc: externalSortByPinyinDesc
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { allTags } = useTags()
|
const { allTags } = useTags()
|
||||||
@ -78,14 +82,19 @@ const AssistantItem: FC<AssistantItemProps> = ({
|
|||||||
setIsPending(hasPending)
|
setIsPending(hasPending)
|
||||||
}, [isActive, assistant.topics])
|
}, [isActive, assistant.topics])
|
||||||
|
|
||||||
const sortByPinyinAsc = useCallback(() => {
|
// Local sort functions
|
||||||
|
const localSortByPinyinAsc = useCallback(() => {
|
||||||
updateAssistants(sortAssistantsByPinyin(assistants, true))
|
updateAssistants(sortAssistantsByPinyin(assistants, true))
|
||||||
}, [assistants, updateAssistants])
|
}, [assistants, updateAssistants])
|
||||||
|
|
||||||
const sortByPinyinDesc = useCallback(() => {
|
const localSortByPinyinDesc = useCallback(() => {
|
||||||
updateAssistants(sortAssistantsByPinyin(assistants, false))
|
updateAssistants(sortAssistantsByPinyin(assistants, false))
|
||||||
}, [assistants, updateAssistants])
|
}, [assistants, updateAssistants])
|
||||||
|
|
||||||
|
// Use external sort functions if provided, otherwise use local ones
|
||||||
|
const sortByPinyinAsc = externalSortByPinyinAsc || localSortByPinyinAsc
|
||||||
|
const sortByPinyinDesc = externalSortByPinyinDesc || localSortByPinyinDesc
|
||||||
|
|
||||||
const menuItems = useMemo(
|
const menuItems = useMemo(
|
||||||
() =>
|
() =>
|
||||||
getMenuItems({
|
getMenuItems({
|
||||||
@ -145,7 +154,7 @@ const AssistantItem: FC<AssistantItemProps> = ({
|
|||||||
menu={{ items: menuItems }}
|
menu={{ items: menuItems }}
|
||||||
trigger={['contextMenu']}
|
trigger={['contextMenu']}
|
||||||
popupRender={(menu) => <div onPointerDown={(e) => e.stopPropagation()}>{menu}</div>}>
|
popupRender={(menu) => <div onPointerDown={(e) => e.stopPropagation()}>{menu}</div>}>
|
||||||
<Container onClick={handleSwitch} className={isActive ? 'active' : ''}>
|
<Container onClick={handleSwitch} isActive={isActive}>
|
||||||
<AssistantNameRow className="name" title={fullAssistantName}>
|
<AssistantNameRow className="name" title={fullAssistantName}>
|
||||||
{assistantIconType === 'model' ? (
|
{assistantIconType === 'model' ? (
|
||||||
<ModelAvatar
|
<ModelAvatar
|
||||||
@ -380,65 +389,75 @@ function getMenuItems({
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
const Container = styled.div`
|
const Container = ({
|
||||||
display: flex;
|
children,
|
||||||
flex-direction: row;
|
isActive,
|
||||||
justify-content: space-between;
|
className,
|
||||||
padding: 0 8px;
|
...props
|
||||||
height: 37px;
|
}: PropsWithChildren<{ isActive?: boolean } & React.HTMLAttributes<HTMLDivElement>>) => (
|
||||||
position: relative;
|
<div
|
||||||
border-radius: var(--list-item-border-radius);
|
{...props}
|
||||||
border: 0.5px solid transparent;
|
className={cn(
|
||||||
width: calc(var(--assistants-width) - 20px);
|
'relative flex h-[37px] w-[calc(var(--assistants-width)-20px)] cursor-pointer flex-row justify-between rounded-[var(--list-item-border-radius)] border-[0.5px] border-transparent px-2 hover:bg-[var(--color-list-item-hover)]',
|
||||||
cursor: pointer;
|
isActive && 'bg-[var(--color-list-item)] shadow-[0_1px_2px_0_rgba(0,0,0,0.05)]',
|
||||||
|
className
|
||||||
|
)}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
&:hover {
|
const AssistantNameRow = ({
|
||||||
background-color: var(--color-list-item-hover);
|
children,
|
||||||
}
|
className,
|
||||||
&.active {
|
...props
|
||||||
background-color: var(--color-list-item);
|
}: PropsWithChildren<{} & React.HTMLAttributes<HTMLDivElement>>) => (
|
||||||
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
|
<div
|
||||||
}
|
{...props}
|
||||||
`
|
className={cn('flex min-w-0 flex-1 flex-row items-center gap-2 text-[13px] text-[var(--color-text)]', className)}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
const AssistantNameRow = styled.div`
|
const AssistantName = ({
|
||||||
color: var(--color-text);
|
children,
|
||||||
font-size: 13px;
|
className,
|
||||||
display: flex;
|
...props
|
||||||
flex-direction: row;
|
}: PropsWithChildren<{} & React.HTMLAttributes<HTMLDivElement>>) => (
|
||||||
align-items: center;
|
<div
|
||||||
gap: 8px;
|
{...props}
|
||||||
`
|
className={cn('min-w-0 flex-1 overflow-hidden text-ellipsis whitespace-nowrap text-[13px]', className)}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
const AssistantName = styled.div`
|
const MenuButton = ({
|
||||||
font-size: 13px;
|
children,
|
||||||
`
|
className,
|
||||||
|
...props
|
||||||
|
}: PropsWithChildren<{} & React.HTMLAttributes<HTMLDivElement>>) => (
|
||||||
|
<div
|
||||||
|
{...props}
|
||||||
|
className={cn(
|
||||||
|
'absolute top-[6px] right-[9px] flex h-[22px] min-h-[22px] min-w-[22px] flex-row items-center justify-center rounded-[11px] border-[0.5px] border-[var(--color-border)] bg-[var(--color-background)] px-[5px]',
|
||||||
|
className
|
||||||
|
)}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
const MenuButton = styled.div`
|
const TopicCount = ({
|
||||||
display: flex;
|
children,
|
||||||
flex-direction: row;
|
className,
|
||||||
justify-content: center;
|
...props
|
||||||
align-items: center;
|
}: PropsWithChildren<{} & React.HTMLAttributes<HTMLDivElement>>) => (
|
||||||
min-width: 22px;
|
<div
|
||||||
height: 22px;
|
{...props}
|
||||||
min-height: 22px;
|
className={cn(
|
||||||
border-radius: 11px;
|
'flex flex-row items-center justify-center rounded-[10px] text-[10px] text-[var(--color-text)]',
|
||||||
position: absolute;
|
className
|
||||||
background-color: var(--color-background);
|
)}>
|
||||||
right: 9px;
|
{children}
|
||||||
top: 6px;
|
</div>
|
||||||
padding: 0 5px;
|
)
|
||||||
border: 0.5px solid var(--color-border);
|
|
||||||
`
|
|
||||||
|
|
||||||
const TopicCount = styled.div`
|
|
||||||
color: var(--color-text);
|
|
||||||
font-size: 10px;
|
|
||||||
border-radius: 10px;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
`
|
|
||||||
|
|
||||||
export default memo(AssistantItem)
|
export default memo(AssistantItem)
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { Alert, Button, Spinner } from '@heroui/react'
|
import { Alert, Spinner } from '@heroui/react'
|
||||||
import { DynamicVirtualList } from '@renderer/components/VirtualList'
|
import { DynamicVirtualList } from '@renderer/components/VirtualList'
|
||||||
import { useAgent } from '@renderer/hooks/agents/useAgent'
|
import { useAgent } from '@renderer/hooks/agents/useAgent'
|
||||||
import { useSessions } from '@renderer/hooks/agents/useSessions'
|
import { useSessions } from '@renderer/hooks/agents/useSessions'
|
||||||
@ -13,10 +13,10 @@ import {
|
|||||||
import { CreateSessionForm } from '@renderer/types'
|
import { CreateSessionForm } from '@renderer/types'
|
||||||
import { buildAgentSessionTopicId } from '@renderer/utils/agentSession'
|
import { buildAgentSessionTopicId } from '@renderer/utils/agentSession'
|
||||||
import { AnimatePresence, motion } from 'framer-motion'
|
import { AnimatePresence, motion } from 'framer-motion'
|
||||||
import { Plus } from 'lucide-react'
|
|
||||||
import { memo, useCallback, useEffect } from 'react'
|
import { memo, useCallback, useEffect } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
|
import AddButton from './AddButton'
|
||||||
import SessionItem from './SessionItem'
|
import SessionItem from './SessionItem'
|
||||||
|
|
||||||
// const logger = loggerService.withContext('SessionsTab')
|
// const logger = loggerService.withContext('SessionsTab')
|
||||||
@ -115,12 +115,9 @@ const Sessions: React.FC<SessionsProps> = ({ agentId }) => {
|
|||||||
transition={{ duration: 0.3 }}
|
transition={{ duration: 0.3 }}
|
||||||
className="sessions-tab flex h-full w-full flex-col p-2">
|
className="sessions-tab flex h-full w-full flex-col p-2">
|
||||||
<motion.div initial={{ opacity: 0, y: -10 }} animate={{ opacity: 1, y: 0 }}>
|
<motion.div initial={{ opacity: 0, y: -10 }} animate={{ opacity: 1, y: 0 }}>
|
||||||
<Button
|
<AddButton onPress={handleCreateSession} className="mb-2">
|
||||||
onPress={handleCreateSession}
|
|
||||||
className="mb-2 w-full justify-start bg-transparent text-foreground-500 hover:bg-accent">
|
|
||||||
<Plus size={16} className="mr-1 shrink-0" />
|
|
||||||
{t('agent.session.add.title')}
|
{t('agent.session.add.title')}
|
||||||
</Button>
|
</AddButton>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
<AnimatePresence>
|
<AnimatePresence>
|
||||||
{/* h-9 */}
|
{/* h-9 */}
|
||||||
|
|||||||
63
src/renderer/src/pages/home/Tabs/components/TagGroup.tsx
Normal file
63
src/renderer/src/pages/home/Tabs/components/TagGroup.tsx
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
import { DownOutlined, RightOutlined } from '@ant-design/icons'
|
||||||
|
import { cn } from '@heroui/react'
|
||||||
|
import { Tooltip } from 'antd'
|
||||||
|
import { FC, ReactNode } from 'react'
|
||||||
|
|
||||||
|
interface TagGroupProps {
|
||||||
|
tag: string
|
||||||
|
isCollapsed: boolean
|
||||||
|
onToggle: (tag: string) => void
|
||||||
|
showTitle?: boolean
|
||||||
|
children: ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TagGroup: FC<TagGroupProps> = ({ tag, isCollapsed, onToggle, showTitle = true, children }) => {
|
||||||
|
return (
|
||||||
|
<TagsContainer>
|
||||||
|
{showTitle && (
|
||||||
|
<GroupTitle onClick={() => onToggle(tag)}>
|
||||||
|
<Tooltip title={tag}>
|
||||||
|
<GroupTitleName>
|
||||||
|
{isCollapsed ? (
|
||||||
|
<RightOutlined style={{ fontSize: '10px', marginRight: '5px' }} />
|
||||||
|
) : (
|
||||||
|
<DownOutlined style={{ fontSize: '10px', marginRight: '5px' }} />
|
||||||
|
)}
|
||||||
|
{tag}
|
||||||
|
</GroupTitleName>
|
||||||
|
</Tooltip>
|
||||||
|
<GroupTitleDivider />
|
||||||
|
</GroupTitle>
|
||||||
|
)}
|
||||||
|
{!isCollapsed && <div>{children}</div>}
|
||||||
|
</TagsContainer>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const TagsContainer: FC<React.HTMLAttributes<HTMLDivElement>> = ({ children, ...props }) => (
|
||||||
|
<div className={cn('flex flex-col gap-2')} {...props}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
|
const GroupTitle: FC<React.HTMLAttributes<HTMLDivElement>> = ({ children, ...props }) => (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'my-1 flex h-6 cursor-pointer flex-row items-center justify-between font-medium text-[var(--color-text-2)] text-xs'
|
||||||
|
)}
|
||||||
|
{...props}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
|
const GroupTitleName: FC<React.HTMLAttributes<HTMLDivElement>> = ({ children, ...props }) => (
|
||||||
|
<div
|
||||||
|
className={cn('mr-1 box-border flex max-w-[50%] truncate px-1 text-[13px] text-[var(--color-text)] leading-6')}
|
||||||
|
{...props}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
|
const GroupTitleDivider: FC<React.HTMLAttributes<HTMLDivElement>> = (props) => (
|
||||||
|
<div className={cn('flex-1 border-[var(--color-border)] border-t')} {...props} />
|
||||||
|
)
|
||||||
@ -41,7 +41,6 @@ import {
|
|||||||
PackagePlus,
|
PackagePlus,
|
||||||
PinIcon,
|
PinIcon,
|
||||||
PinOffIcon,
|
PinOffIcon,
|
||||||
PlusIcon,
|
|
||||||
Save,
|
Save,
|
||||||
Sparkles,
|
Sparkles,
|
||||||
UploadIcon,
|
UploadIcon,
|
||||||
@ -52,6 +51,8 @@ import { useTranslation } from 'react-i18next'
|
|||||||
import { useDispatch, useSelector } from 'react-redux'
|
import { useDispatch, useSelector } from 'react-redux'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
|
|
||||||
|
import AddButton from './AddButton'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
assistant: Assistant
|
assistant: Assistant
|
||||||
activeTopic: Topic
|
activeTopic: Topic
|
||||||
@ -497,13 +498,12 @@ export const Topics: React.FC<Props> = ({ assistant: _assistant, activeTopic, se
|
|||||||
className="topics-tab"
|
className="topics-tab"
|
||||||
list={sortedTopics}
|
list={sortedTopics}
|
||||||
onUpdate={updateTopics}
|
onUpdate={updateTopics}
|
||||||
style={{ height: '100%', padding: '13px 0 10px 10px' }}
|
style={{ height: '100%', padding: '11px 0 10px 10px' }}
|
||||||
itemContainerStyle={{ paddingBottom: '8px' }}
|
itemContainerStyle={{ paddingBottom: '8px' }}
|
||||||
header={
|
header={
|
||||||
<AddTopicButton onClick={() => EventEmitter.emit(EVENT_NAMES.ADD_NEW_TOPIC)}>
|
<AddButton onPress={() => EventEmitter.emit(EVENT_NAMES.ADD_NEW_TOPIC)} className="mb-2">
|
||||||
<PlusIcon size={16} />
|
|
||||||
{t('chat.add.topic.title')}
|
{t('chat.add.topic.title')}
|
||||||
</AddTopicButton>
|
</AddButton>
|
||||||
}>
|
}>
|
||||||
{(topic) => {
|
{(topic) => {
|
||||||
const isActive = topic.id === activeTopic?.id
|
const isActive = topic.id === activeTopic?.id
|
||||||
@ -740,31 +740,6 @@ const FulfilledIndicator = styled.div.attrs({
|
|||||||
background-color: var(--color-status-success);
|
background-color: var(--color-status-success);
|
||||||
`
|
`
|
||||||
|
|
||||||
const AddTopicButton = styled.div`
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 6px;
|
|
||||||
width: calc(100% - 10px);
|
|
||||||
padding: 7px 12px;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
background: transparent;
|
|
||||||
color: var(--color-text-2);
|
|
||||||
font-size: 13px;
|
|
||||||
border-radius: var(--list-item-border-radius);
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.2s;
|
|
||||||
margin-top: -5px;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background-color: var(--color-list-item-hover);
|
|
||||||
color: var(--color-text-1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.anticon {
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
`
|
|
||||||
|
|
||||||
const TopicPromptText = styled.div`
|
const TopicPromptText = styled.div`
|
||||||
color: var(--color-text-2);
|
color: var(--color-text-2);
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
|
|||||||
@ -0,0 +1,61 @@
|
|||||||
|
import { Button, Popover, PopoverContent, PopoverTrigger } from '@heroui/react'
|
||||||
|
import { AgentModal } from '@renderer/components/Popups/agent/AgentModal'
|
||||||
|
import { Bot, MessageSquare } from 'lucide-react'
|
||||||
|
import { FC, useState } from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
|
import AddButton from './AddButton'
|
||||||
|
|
||||||
|
interface UnifiedAddButtonProps {
|
||||||
|
onCreateAssistant: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const UnifiedAddButton: FC<UnifiedAddButtonProps> = ({ onCreateAssistant }) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const [isPopoverOpen, setIsPopoverOpen] = useState(false)
|
||||||
|
const [isAgentModalOpen, setIsAgentModalOpen] = useState(false)
|
||||||
|
|
||||||
|
const handleAddAssistant = () => {
|
||||||
|
setIsPopoverOpen(false)
|
||||||
|
onCreateAssistant()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleAddAgent = () => {
|
||||||
|
setIsPopoverOpen(false)
|
||||||
|
setIsAgentModalOpen(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mb-1">
|
||||||
|
<Popover
|
||||||
|
isOpen={isPopoverOpen}
|
||||||
|
onOpenChange={setIsPopoverOpen}
|
||||||
|
placement="bottom"
|
||||||
|
classNames={{ content: 'p-0 min-w-[200px]' }}>
|
||||||
|
<PopoverTrigger>
|
||||||
|
<AddButton>{t('chat.add.assistant.title')}</AddButton>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent>
|
||||||
|
<div className="flex w-full flex-col gap-1 p-1">
|
||||||
|
<Button
|
||||||
|
onPress={handleAddAssistant}
|
||||||
|
className="w-full justify-start bg-transparent hover:bg-[var(--color-list-item)]"
|
||||||
|
startContent={<MessageSquare size={16} className="shrink-0" />}>
|
||||||
|
{t('chat.add.assistant.title')}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onPress={handleAddAgent}
|
||||||
|
className="w-full justify-start bg-transparent hover:bg-[var(--color-list-item)]"
|
||||||
|
startContent={<Bot size={16} className="shrink-0" />}>
|
||||||
|
{t('agent.add.title')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
|
||||||
|
<AgentModal isOpen={isAgentModalOpen} onClose={() => setIsAgentModalOpen(false)} />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default UnifiedAddButton
|
||||||
108
src/renderer/src/pages/home/Tabs/components/UnifiedList.tsx
Normal file
108
src/renderer/src/pages/home/Tabs/components/UnifiedList.tsx
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
import { DraggableList } from '@renderer/components/DraggableList'
|
||||||
|
import { Assistant, AssistantsSortType } from '@renderer/types'
|
||||||
|
import { FC, useCallback } from 'react'
|
||||||
|
|
||||||
|
import { UnifiedItem } from '../hooks/useUnifiedItems'
|
||||||
|
import AgentItem from './AgentItem'
|
||||||
|
import AssistantItem from './AssistantItem'
|
||||||
|
|
||||||
|
interface UnifiedListProps {
|
||||||
|
items: UnifiedItem[]
|
||||||
|
activeAssistantId: string
|
||||||
|
activeAgentId: string | null
|
||||||
|
sortBy: AssistantsSortType
|
||||||
|
onReorder: (newList: UnifiedItem[]) => void
|
||||||
|
onDragStart: () => void
|
||||||
|
onDragEnd: () => void
|
||||||
|
onAssistantSwitch: (assistant: Assistant) => void
|
||||||
|
onAssistantDelete: (assistant: Assistant) => void
|
||||||
|
onAgentDelete: (agentId: string) => void
|
||||||
|
onAgentPress: (agentId: string) => void
|
||||||
|
addPreset: (assistant: Assistant) => void
|
||||||
|
copyAssistant: (assistant: Assistant) => void
|
||||||
|
onCreateDefaultAssistant: () => void
|
||||||
|
handleSortByChange: (sortType: AssistantsSortType) => void
|
||||||
|
sortByPinyinAsc: () => void
|
||||||
|
sortByPinyinDesc: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export const UnifiedList: FC<UnifiedListProps> = (props) => {
|
||||||
|
const {
|
||||||
|
items,
|
||||||
|
activeAssistantId,
|
||||||
|
activeAgentId,
|
||||||
|
sortBy,
|
||||||
|
onReorder,
|
||||||
|
onDragStart,
|
||||||
|
onDragEnd,
|
||||||
|
onAssistantSwitch,
|
||||||
|
onAssistantDelete,
|
||||||
|
onAgentDelete,
|
||||||
|
onAgentPress,
|
||||||
|
addPreset,
|
||||||
|
copyAssistant,
|
||||||
|
onCreateDefaultAssistant,
|
||||||
|
handleSortByChange,
|
||||||
|
sortByPinyinAsc,
|
||||||
|
sortByPinyinDesc
|
||||||
|
} = props
|
||||||
|
|
||||||
|
const renderUnifiedItem = useCallback(
|
||||||
|
(item: UnifiedItem) => {
|
||||||
|
if (item.type === 'agent') {
|
||||||
|
return (
|
||||||
|
<AgentItem
|
||||||
|
key={`agent-${item.data.id}`}
|
||||||
|
agent={item.data}
|
||||||
|
isActive={item.data.id === activeAgentId}
|
||||||
|
onDelete={() => onAgentDelete(item.data.id)}
|
||||||
|
onPress={() => onAgentPress(item.data.id)}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<AssistantItem
|
||||||
|
key={`assistant-${item.data.id}`}
|
||||||
|
assistant={item.data}
|
||||||
|
isActive={item.data.id === activeAssistantId}
|
||||||
|
sortBy={sortBy}
|
||||||
|
onSwitch={onAssistantSwitch}
|
||||||
|
onDelete={onAssistantDelete}
|
||||||
|
addPreset={addPreset}
|
||||||
|
copyAssistant={copyAssistant}
|
||||||
|
onCreateDefaultAssistant={onCreateDefaultAssistant}
|
||||||
|
handleSortByChange={handleSortByChange}
|
||||||
|
sortByPinyinAsc={sortByPinyinAsc}
|
||||||
|
sortByPinyinDesc={sortByPinyinDesc}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[
|
||||||
|
activeAgentId,
|
||||||
|
activeAssistantId,
|
||||||
|
sortBy,
|
||||||
|
onAssistantSwitch,
|
||||||
|
onAssistantDelete,
|
||||||
|
onAgentDelete,
|
||||||
|
onAgentPress,
|
||||||
|
addPreset,
|
||||||
|
copyAssistant,
|
||||||
|
onCreateDefaultAssistant,
|
||||||
|
handleSortByChange,
|
||||||
|
sortByPinyinAsc,
|
||||||
|
sortByPinyinDesc
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DraggableList
|
||||||
|
list={items}
|
||||||
|
itemKey={(item) => `${item.type}-${item.data.id}`}
|
||||||
|
onUpdate={onReorder}
|
||||||
|
onDragStart={onDragStart}
|
||||||
|
onDragEnd={onDragEnd}>
|
||||||
|
{renderUnifiedItem}
|
||||||
|
</DraggableList>
|
||||||
|
)
|
||||||
|
}
|
||||||
132
src/renderer/src/pages/home/Tabs/components/UnifiedTagGroups.tsx
Normal file
132
src/renderer/src/pages/home/Tabs/components/UnifiedTagGroups.tsx
Normal file
@ -0,0 +1,132 @@
|
|||||||
|
import { DraggableList } from '@renderer/components/DraggableList'
|
||||||
|
import { Assistant, AssistantsSortType } from '@renderer/types'
|
||||||
|
import { FC, useCallback } from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
|
import { UnifiedItem } from '../hooks/useUnifiedItems'
|
||||||
|
import AgentItem from './AgentItem'
|
||||||
|
import AssistantItem from './AssistantItem'
|
||||||
|
import { TagGroup } from './TagGroup'
|
||||||
|
|
||||||
|
interface GroupedItems {
|
||||||
|
tag: string
|
||||||
|
items: UnifiedItem[]
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UnifiedTagGroupsProps {
|
||||||
|
groupedItems: GroupedItems[]
|
||||||
|
activeAssistantId: string
|
||||||
|
activeAgentId: string | null
|
||||||
|
sortBy: AssistantsSortType
|
||||||
|
collapsedTags: Record<string, boolean>
|
||||||
|
onGroupReorder: (tag: string, newList: UnifiedItem[]) => void
|
||||||
|
onDragStart: () => void
|
||||||
|
onDragEnd: () => void
|
||||||
|
onToggleTagCollapse: (tag: string) => void
|
||||||
|
onAssistantSwitch: (assistant: Assistant) => void
|
||||||
|
onAssistantDelete: (assistant: Assistant) => void
|
||||||
|
onAgentDelete: (agentId: string) => void
|
||||||
|
onAgentPress: (agentId: string) => void
|
||||||
|
addPreset: (assistant: Assistant) => void
|
||||||
|
copyAssistant: (assistant: Assistant) => void
|
||||||
|
onCreateDefaultAssistant: () => void
|
||||||
|
handleSortByChange: (sortType: AssistantsSortType) => void
|
||||||
|
sortByPinyinAsc: () => void
|
||||||
|
sortByPinyinDesc: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export const UnifiedTagGroups: FC<UnifiedTagGroupsProps> = (props) => {
|
||||||
|
const {
|
||||||
|
groupedItems,
|
||||||
|
activeAssistantId,
|
||||||
|
activeAgentId,
|
||||||
|
sortBy,
|
||||||
|
collapsedTags,
|
||||||
|
onGroupReorder,
|
||||||
|
onDragStart,
|
||||||
|
onDragEnd,
|
||||||
|
onToggleTagCollapse,
|
||||||
|
onAssistantSwitch,
|
||||||
|
onAssistantDelete,
|
||||||
|
onAgentDelete,
|
||||||
|
onAgentPress,
|
||||||
|
addPreset,
|
||||||
|
copyAssistant,
|
||||||
|
onCreateDefaultAssistant,
|
||||||
|
handleSortByChange,
|
||||||
|
sortByPinyinAsc,
|
||||||
|
sortByPinyinDesc
|
||||||
|
} = props
|
||||||
|
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
const renderUnifiedItem = useCallback(
|
||||||
|
(item: UnifiedItem) => {
|
||||||
|
if (item.type === 'agent') {
|
||||||
|
return (
|
||||||
|
<AgentItem
|
||||||
|
key={`agent-${item.data.id}`}
|
||||||
|
agent={item.data}
|
||||||
|
isActive={item.data.id === activeAgentId}
|
||||||
|
onDelete={() => onAgentDelete(item.data.id)}
|
||||||
|
onPress={() => onAgentPress(item.data.id)}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<AssistantItem
|
||||||
|
key={`assistant-${item.data.id}`}
|
||||||
|
assistant={item.data}
|
||||||
|
isActive={item.data.id === activeAssistantId}
|
||||||
|
sortBy={sortBy}
|
||||||
|
onSwitch={onAssistantSwitch}
|
||||||
|
onDelete={onAssistantDelete}
|
||||||
|
addPreset={addPreset}
|
||||||
|
copyAssistant={copyAssistant}
|
||||||
|
onCreateDefaultAssistant={onCreateDefaultAssistant}
|
||||||
|
handleSortByChange={handleSortByChange}
|
||||||
|
sortByPinyinAsc={sortByPinyinAsc}
|
||||||
|
sortByPinyinDesc={sortByPinyinDesc}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[
|
||||||
|
activeAgentId,
|
||||||
|
activeAssistantId,
|
||||||
|
sortBy,
|
||||||
|
onAssistantSwitch,
|
||||||
|
onAssistantDelete,
|
||||||
|
onAgentDelete,
|
||||||
|
onAgentPress,
|
||||||
|
addPreset,
|
||||||
|
copyAssistant,
|
||||||
|
onCreateDefaultAssistant,
|
||||||
|
handleSortByChange,
|
||||||
|
sortByPinyinAsc,
|
||||||
|
sortByPinyinDesc
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{groupedItems.map((group) => (
|
||||||
|
<TagGroup
|
||||||
|
key={group.tag}
|
||||||
|
tag={group.tag}
|
||||||
|
isCollapsed={collapsedTags[group.tag]}
|
||||||
|
onToggle={onToggleTagCollapse}
|
||||||
|
showTitle={group.tag !== t('assistants.tags.untagged')}>
|
||||||
|
<DraggableList
|
||||||
|
list={group.items}
|
||||||
|
itemKey={(item) => `${item.type}-${item.data.id}`}
|
||||||
|
onUpdate={(newList) => onGroupReorder(group.tag, newList)}
|
||||||
|
onDragStart={onDragStart}
|
||||||
|
onDragEnd={onDragEnd}>
|
||||||
|
{renderUnifiedItem}
|
||||||
|
</DraggableList>
|
||||||
|
</TagGroup>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
19
src/renderer/src/pages/home/Tabs/hooks/useActiveAgent.ts
Normal file
19
src/renderer/src/pages/home/Tabs/hooks/useActiveAgent.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import { useAgentSessionInitializer } from '@renderer/hooks/agents/useAgentSessionInitializer'
|
||||||
|
import { useAppDispatch } from '@renderer/store'
|
||||||
|
import { setActiveAgentId as setActiveAgentIdAction } from '@renderer/store/runtime'
|
||||||
|
import { useCallback } from 'react'
|
||||||
|
|
||||||
|
export const useActiveAgent = () => {
|
||||||
|
const dispatch = useAppDispatch()
|
||||||
|
const { initializeAgentSession } = useAgentSessionInitializer()
|
||||||
|
|
||||||
|
const setActiveAgentId = useCallback(
|
||||||
|
async (id: string) => {
|
||||||
|
dispatch(setActiveAgentIdAction(id))
|
||||||
|
await initializeAgentSession(id)
|
||||||
|
},
|
||||||
|
[dispatch, initializeAgentSession]
|
||||||
|
)
|
||||||
|
|
||||||
|
return { setActiveAgentId }
|
||||||
|
}
|
||||||
140
src/renderer/src/pages/home/Tabs/hooks/useUnifiedGrouping.ts
Normal file
140
src/renderer/src/pages/home/Tabs/hooks/useUnifiedGrouping.ts
Normal file
@ -0,0 +1,140 @@
|
|||||||
|
import { useAppDispatch } from '@renderer/store'
|
||||||
|
import { setUnifiedListOrder } from '@renderer/store/assistants'
|
||||||
|
import { AgentEntity, Assistant } from '@renderer/types'
|
||||||
|
import { useCallback, useMemo } from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
|
import { UnifiedItem } from './useUnifiedItems'
|
||||||
|
|
||||||
|
interface UseUnifiedGroupingOptions {
|
||||||
|
unifiedItems: UnifiedItem[]
|
||||||
|
assistants: Assistant[]
|
||||||
|
agents: AgentEntity[]
|
||||||
|
apiServerEnabled: boolean
|
||||||
|
agentsLoading: boolean
|
||||||
|
agentsError: Error | null
|
||||||
|
updateAssistants: (assistants: Assistant[]) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useUnifiedGrouping = (options: UseUnifiedGroupingOptions) => {
|
||||||
|
const { unifiedItems, assistants, agents, apiServerEnabled, agentsLoading, agentsError, updateAssistants } = options
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const dispatch = useAppDispatch()
|
||||||
|
|
||||||
|
// Group unified items by tags
|
||||||
|
const groupedUnifiedItems = useMemo(() => {
|
||||||
|
const groups = new Map<string, UnifiedItem[]>()
|
||||||
|
|
||||||
|
unifiedItems.forEach((item) => {
|
||||||
|
if (item.type === 'agent') {
|
||||||
|
// Agents go to untagged group
|
||||||
|
const groupKey = t('assistants.tags.untagged')
|
||||||
|
if (!groups.has(groupKey)) {
|
||||||
|
groups.set(groupKey, [])
|
||||||
|
}
|
||||||
|
groups.get(groupKey)!.push(item)
|
||||||
|
} else {
|
||||||
|
// Assistants use their tags
|
||||||
|
const tags = item.data.tags?.length ? item.data.tags : [t('assistants.tags.untagged')]
|
||||||
|
tags.forEach((tag) => {
|
||||||
|
if (!groups.has(tag)) {
|
||||||
|
groups.set(tag, [])
|
||||||
|
}
|
||||||
|
groups.get(tag)!.push(item)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Sort groups: untagged first, then tagged groups
|
||||||
|
const untaggedKey = t('assistants.tags.untagged')
|
||||||
|
const sortedGroups = Array.from(groups.entries()).sort(([tagA], [tagB]) => {
|
||||||
|
if (tagA === untaggedKey) return -1
|
||||||
|
if (tagB === untaggedKey) return 1
|
||||||
|
return 0
|
||||||
|
})
|
||||||
|
|
||||||
|
return sortedGroups.map(([tag, items]) => ({ tag, items }))
|
||||||
|
}, [unifiedItems, t])
|
||||||
|
|
||||||
|
const handleUnifiedGroupReorder = useCallback(
|
||||||
|
(tag: string, newGroupList: UnifiedItem[]) => {
|
||||||
|
// Extract only assistants from the new list for updating
|
||||||
|
const newAssistants = newGroupList.filter((item) => item.type === 'assistant').map((item) => item.data)
|
||||||
|
|
||||||
|
// Update assistants state
|
||||||
|
let insertIndex = 0
|
||||||
|
const updatedAssistants = assistants.map((a) => {
|
||||||
|
const tags = a.tags?.length ? a.tags : [t('assistants.tags.untagged')]
|
||||||
|
if (tags.includes(tag)) {
|
||||||
|
const replaced = newAssistants[insertIndex]
|
||||||
|
insertIndex += 1
|
||||||
|
return replaced || a
|
||||||
|
}
|
||||||
|
return a
|
||||||
|
})
|
||||||
|
updateAssistants(updatedAssistants)
|
||||||
|
|
||||||
|
// Rebuild unified order and save to Redux
|
||||||
|
const newUnifiedItems: UnifiedItem[] = []
|
||||||
|
const availableAgents = new Map<string, AgentEntity>()
|
||||||
|
const availableAssistants = new Map<string, Assistant>()
|
||||||
|
|
||||||
|
if (apiServerEnabled && !agentsLoading && !agentsError) {
|
||||||
|
agents.forEach((agent) => availableAgents.set(agent.id, agent))
|
||||||
|
}
|
||||||
|
updatedAssistants.forEach((assistant) => availableAssistants.set(assistant.id, assistant))
|
||||||
|
|
||||||
|
// Reconstruct order based on current groupedUnifiedItems structure
|
||||||
|
groupedUnifiedItems.forEach((group) => {
|
||||||
|
if (group.tag === tag) {
|
||||||
|
// Use the new group list for this tag
|
||||||
|
newGroupList.forEach((item) => {
|
||||||
|
newUnifiedItems.push(item)
|
||||||
|
if (item.type === 'agent') {
|
||||||
|
availableAgents.delete(item.data.id)
|
||||||
|
} else {
|
||||||
|
availableAssistants.delete(item.data.id)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
// Keep existing order for other tags
|
||||||
|
group.items.forEach((item) => {
|
||||||
|
newUnifiedItems.push(item)
|
||||||
|
if (item.type === 'agent') {
|
||||||
|
availableAgents.delete(item.data.id)
|
||||||
|
} else {
|
||||||
|
availableAssistants.delete(item.data.id)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Add any remaining items
|
||||||
|
availableAgents.forEach((agent) => newUnifiedItems.push({ type: 'agent', data: agent }))
|
||||||
|
availableAssistants.forEach((assistant) => newUnifiedItems.push({ type: 'assistant', data: assistant }))
|
||||||
|
|
||||||
|
// Save to Redux
|
||||||
|
const orderToSave = newUnifiedItems.map((item) => ({
|
||||||
|
type: item.type,
|
||||||
|
id: item.data.id
|
||||||
|
}))
|
||||||
|
dispatch(setUnifiedListOrder(orderToSave))
|
||||||
|
},
|
||||||
|
[
|
||||||
|
assistants,
|
||||||
|
t,
|
||||||
|
updateAssistants,
|
||||||
|
apiServerEnabled,
|
||||||
|
agentsLoading,
|
||||||
|
agentsError,
|
||||||
|
agents,
|
||||||
|
groupedUnifiedItems,
|
||||||
|
dispatch
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
groupedUnifiedItems,
|
||||||
|
handleUnifiedGroupReorder
|
||||||
|
}
|
||||||
|
}
|
||||||
73
src/renderer/src/pages/home/Tabs/hooks/useUnifiedItems.ts
Normal file
73
src/renderer/src/pages/home/Tabs/hooks/useUnifiedItems.ts
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
import { useAppDispatch, useAppSelector } from '@renderer/store'
|
||||||
|
import { setUnifiedListOrder } from '@renderer/store/assistants'
|
||||||
|
import { AgentEntity, Assistant } from '@renderer/types'
|
||||||
|
import { useCallback, useMemo } from 'react'
|
||||||
|
|
||||||
|
export type UnifiedItem = { type: 'agent'; data: AgentEntity } | { type: 'assistant'; data: Assistant }
|
||||||
|
|
||||||
|
interface UseUnifiedItemsOptions {
|
||||||
|
agents: AgentEntity[]
|
||||||
|
assistants: Assistant[]
|
||||||
|
apiServerEnabled: boolean
|
||||||
|
agentsLoading: boolean
|
||||||
|
agentsError: Error | null
|
||||||
|
updateAssistants: (assistants: Assistant[]) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useUnifiedItems = (options: UseUnifiedItemsOptions) => {
|
||||||
|
const { agents, assistants, apiServerEnabled, agentsLoading, agentsError, updateAssistants } = options
|
||||||
|
const dispatch = useAppDispatch()
|
||||||
|
const unifiedListOrder = useAppSelector((state) => state.assistants.unifiedListOrder || [])
|
||||||
|
|
||||||
|
// Create unified items list (agents + assistants) with saved order
|
||||||
|
const unifiedItems = useMemo(() => {
|
||||||
|
const items: UnifiedItem[] = []
|
||||||
|
|
||||||
|
// Collect all available items
|
||||||
|
const availableAgents = new Map<string, AgentEntity>()
|
||||||
|
const availableAssistants = new Map<string, Assistant>()
|
||||||
|
|
||||||
|
if (apiServerEnabled && !agentsLoading && !agentsError) {
|
||||||
|
agents.forEach((agent) => availableAgents.set(agent.id, agent))
|
||||||
|
}
|
||||||
|
assistants.forEach((assistant) => availableAssistants.set(assistant.id, assistant))
|
||||||
|
|
||||||
|
// Apply saved order
|
||||||
|
unifiedListOrder.forEach((item) => {
|
||||||
|
if (item.type === 'agent' && availableAgents.has(item.id)) {
|
||||||
|
items.push({ type: 'agent', data: availableAgents.get(item.id)! })
|
||||||
|
availableAgents.delete(item.id)
|
||||||
|
} else if (item.type === 'assistant' && availableAssistants.has(item.id)) {
|
||||||
|
items.push({ type: 'assistant', data: availableAssistants.get(item.id)! })
|
||||||
|
availableAssistants.delete(item.id)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Add new items (not in saved order) to the end
|
||||||
|
availableAgents.forEach((agent) => items.push({ type: 'agent', data: agent }))
|
||||||
|
availableAssistants.forEach((assistant) => items.push({ type: 'assistant', data: assistant }))
|
||||||
|
|
||||||
|
return items
|
||||||
|
}, [agents, assistants, apiServerEnabled, agentsLoading, agentsError, unifiedListOrder])
|
||||||
|
|
||||||
|
const handleUnifiedListReorder = useCallback(
|
||||||
|
(newList: UnifiedItem[]) => {
|
||||||
|
// Save the unified order to Redux
|
||||||
|
const orderToSave = newList.map((item) => ({
|
||||||
|
type: item.type,
|
||||||
|
id: item.data.id
|
||||||
|
}))
|
||||||
|
dispatch(setUnifiedListOrder(orderToSave))
|
||||||
|
|
||||||
|
// Extract and update assistants order
|
||||||
|
const newAssistants = newList.filter((item) => item.type === 'assistant').map((item) => item.data)
|
||||||
|
updateAssistants(newAssistants)
|
||||||
|
},
|
||||||
|
[dispatch, updateAssistants]
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
unifiedItems,
|
||||||
|
handleUnifiedListReorder
|
||||||
|
}
|
||||||
|
}
|
||||||
56
src/renderer/src/pages/home/Tabs/hooks/useUnifiedSorting.ts
Normal file
56
src/renderer/src/pages/home/Tabs/hooks/useUnifiedSorting.ts
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
import { useAppDispatch } from '@renderer/store'
|
||||||
|
import { setUnifiedListOrder } from '@renderer/store/assistants'
|
||||||
|
import { Assistant } from '@renderer/types'
|
||||||
|
import { useCallback } from 'react'
|
||||||
|
import * as tinyPinyin from 'tiny-pinyin'
|
||||||
|
|
||||||
|
import { UnifiedItem } from './useUnifiedItems'
|
||||||
|
|
||||||
|
interface UseUnifiedSortingOptions {
|
||||||
|
unifiedItems: UnifiedItem[]
|
||||||
|
updateAssistants: (assistants: Assistant[]) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useUnifiedSorting = (options: UseUnifiedSortingOptions) => {
|
||||||
|
const { unifiedItems, updateAssistants } = options
|
||||||
|
const dispatch = useAppDispatch()
|
||||||
|
|
||||||
|
const sortUnifiedItemsByPinyin = useCallback((items: UnifiedItem[], isAscending: boolean) => {
|
||||||
|
return [...items].sort((a, b) => {
|
||||||
|
const nameA = a.type === 'agent' ? a.data.name || a.data.id : a.data.name
|
||||||
|
const nameB = b.type === 'agent' ? b.data.name || b.data.id : b.data.name
|
||||||
|
const pinyinA = tinyPinyin.convertToPinyin(nameA, '', true)
|
||||||
|
const pinyinB = tinyPinyin.convertToPinyin(nameB, '', true)
|
||||||
|
return isAscending ? pinyinA.localeCompare(pinyinB) : pinyinB.localeCompare(pinyinA)
|
||||||
|
})
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const sortByPinyinAsc = useCallback(() => {
|
||||||
|
const sorted = sortUnifiedItemsByPinyin(unifiedItems, true)
|
||||||
|
const orderToSave = sorted.map((item) => ({
|
||||||
|
type: item.type,
|
||||||
|
id: item.data.id
|
||||||
|
}))
|
||||||
|
dispatch(setUnifiedListOrder(orderToSave))
|
||||||
|
// Also update assistants order
|
||||||
|
const newAssistants = sorted.filter((item) => item.type === 'assistant').map((item) => item.data)
|
||||||
|
updateAssistants(newAssistants)
|
||||||
|
}, [unifiedItems, sortUnifiedItemsByPinyin, dispatch, updateAssistants])
|
||||||
|
|
||||||
|
const sortByPinyinDesc = useCallback(() => {
|
||||||
|
const sorted = sortUnifiedItemsByPinyin(unifiedItems, false)
|
||||||
|
const orderToSave = sorted.map((item) => ({
|
||||||
|
type: item.type,
|
||||||
|
id: item.data.id
|
||||||
|
}))
|
||||||
|
dispatch(setUnifiedListOrder(orderToSave))
|
||||||
|
// Also update assistants order
|
||||||
|
const newAssistants = sorted.filter((item) => item.type === 'assistant').map((item) => item.data)
|
||||||
|
updateAssistants(newAssistants)
|
||||||
|
}, [unifiedItems, sortUnifiedItemsByPinyin, dispatch, updateAssistants])
|
||||||
|
|
||||||
|
return {
|
||||||
|
sortByPinyinAsc,
|
||||||
|
sortByPinyinDesc
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -13,6 +13,7 @@ export interface AssistantsState {
|
|||||||
tagsOrder: string[]
|
tagsOrder: string[]
|
||||||
collapsedTags: Record<string, boolean>
|
collapsedTags: Record<string, boolean>
|
||||||
presets: AssistantPreset[]
|
presets: AssistantPreset[]
|
||||||
|
unifiedListOrder: Array<{ type: 'agent' | 'assistant'; id: string }>
|
||||||
}
|
}
|
||||||
|
|
||||||
const initialState: AssistantsState = {
|
const initialState: AssistantsState = {
|
||||||
@ -20,7 +21,8 @@ const initialState: AssistantsState = {
|
|||||||
assistants: [getDefaultAssistant()],
|
assistants: [getDefaultAssistant()],
|
||||||
tagsOrder: [],
|
tagsOrder: [],
|
||||||
collapsedTags: {},
|
collapsedTags: {},
|
||||||
presets: []
|
presets: [],
|
||||||
|
unifiedListOrder: []
|
||||||
}
|
}
|
||||||
|
|
||||||
const assistantsSlice = createSlice({
|
const assistantsSlice = createSlice({
|
||||||
@ -96,6 +98,9 @@ const assistantsSlice = createSlice({
|
|||||||
[tag]: !prev[tag]
|
[tag]: !prev[tag]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
setUnifiedListOrder: (state, action: PayloadAction<Array<{ type: 'agent' | 'assistant'; id: string }>>) => {
|
||||||
|
state.unifiedListOrder = action.payload
|
||||||
|
},
|
||||||
addTopic: (state, action: PayloadAction<{ assistantId: string; topic: Topic }>) => {
|
addTopic: (state, action: PayloadAction<{ assistantId: string; topic: Topic }>) => {
|
||||||
const topic = action.payload.topic
|
const topic = action.payload.topic
|
||||||
topic.createdAt = topic.createdAt || new Date().toISOString()
|
topic.createdAt = topic.createdAt || new Date().toISOString()
|
||||||
@ -244,6 +249,7 @@ export const {
|
|||||||
setTagsOrder,
|
setTagsOrder,
|
||||||
updateAssistantSettings,
|
updateAssistantSettings,
|
||||||
updateTagCollapse,
|
updateTagCollapse,
|
||||||
|
setUnifiedListOrder,
|
||||||
setAssistantPresets,
|
setAssistantPresets,
|
||||||
addAssistantPreset,
|
addAssistantPreset,
|
||||||
removeAssistantPreset,
|
removeAssistantPreset,
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user