mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2025-12-28 21:42:27 +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 { Assistant } from '@renderer/types'
|
||||
import { FC, useRef } from 'react'
|
||||
import { useAgents } from '@renderer/hooks/agents/useAgents'
|
||||
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 { AgentSection } from './components/AgentSection'
|
||||
import Assistants from './components/Assistants'
|
||||
import UnifiedAddButton from './components/UnifiedAddButton'
|
||||
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 {
|
||||
activeAssistant: Assistant
|
||||
@ -13,12 +29,143 @@ interface AssistantsTabProps {
|
||||
onCreateDefaultAssistant: () => void
|
||||
}
|
||||
|
||||
const ALERT_KEY = 'enable_api_server_to_use_agent'
|
||||
|
||||
const AssistantsTab: FC<AssistantsTabProps> = (props) => {
|
||||
const { activeAssistant, setActiveAssistant, onCreateAssistant, onCreateDefaultAssistant } = props
|
||||
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 (
|
||||
<Container className="assistants-tab" ref={containerRef}>
|
||||
<AgentSection />
|
||||
<Assistants {...props} />
|
||||
{!apiServer.enabled && !iknow[ALERT_KEY] && (
|
||||
<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>
|
||||
)
|
||||
}
|
||||
@ -27,7 +174,6 @@ const Container = styled(Scrollbar)`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 10px;
|
||||
margin-top: 3px;
|
||||
`
|
||||
|
||||
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 { useSessions } from '@renderer/hooks/agents/useSessions'
|
||||
import { useSettings } from '@renderer/hooks/useSettings'
|
||||
import AgentSettingsPopup from '@renderer/pages/settings/AgentSettings/AgentSettingsPopup'
|
||||
import { AgentLabel } from '@renderer/pages/settings/AgentSettings/shared'
|
||||
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
|
||||
import { AgentEntity } from '@renderer/types'
|
||||
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'
|
||||
|
||||
// const logger = loggerService.withContext('AgentItem')
|
||||
@ -20,81 +23,107 @@ interface AgentItemProps {
|
||||
const AgentItem: FC<AgentItemProps> = ({ agent, isActive, onDelete, onPress }) => {
|
||||
const { t } = useTranslation()
|
||||
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 (
|
||||
<>
|
||||
<ContextMenu modal={false}>
|
||||
<ContextMenuTrigger>
|
||||
<ButtonContainer onPress={onPress} className={isActive ? 'active' : ''}>
|
||||
<AssistantNameRow className="name flex w-full justify-between" title={agent.name ?? agent.id}>
|
||||
<ContextMenu modal={false}>
|
||||
<ContextMenuTrigger>
|
||||
<Container onClick={handlePress} isActive={isActive}>
|
||||
<AssistantNameRow className="name" title={agent.name ?? agent.id}>
|
||||
<AgentNameWrapper>
|
||||
<AgentLabel agent={agent} />
|
||||
{isActive && (
|
||||
<Chip
|
||||
variant="bordered"
|
||||
size="sm"
|
||||
radius="full"
|
||||
className="aspect-square h-5 w-5 items-center justify-center border-[0.5px] bg-background text-[10px]">
|
||||
{sessions.length}
|
||||
</Chip>
|
||||
)}
|
||||
</AssistantNameRow>
|
||||
</ButtonContainer>
|
||||
</ContextMenuTrigger>
|
||||
<ContextMenuContent>
|
||||
<ContextMenuItem
|
||||
key="edit"
|
||||
onClick={async () => {
|
||||
// onOpen()
|
||||
await AgentSettingsPopup.show({
|
||||
agentId: agent.id
|
||||
})
|
||||
}}>
|
||||
<EditIcon size={14} />
|
||||
{t('common.edit')}
|
||||
</ContextMenuItem>
|
||||
<ContextMenuItem
|
||||
key="delete"
|
||||
className="text-danger"
|
||||
onClick={() => {
|
||||
window.modal.confirm({
|
||||
title: t('agent.delete.title'),
|
||||
content: t('agent.delete.content'),
|
||||
centered: true,
|
||||
okButtonProps: { danger: true },
|
||||
onOk: () => onDelete(agent)
|
||||
})
|
||||
}}>
|
||||
<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} /> */}
|
||||
</>
|
||||
</AgentNameWrapper>
|
||||
</AssistantNameRow>
|
||||
<MenuButton>
|
||||
{isActive ? <SessionCount>{sessions.length}</SessionCount> : <Bot size={12} className="text-primary" />}
|
||||
</MenuButton>
|
||||
</Container>
|
||||
</ContextMenuTrigger>
|
||||
<ContextMenuContent>
|
||||
<ContextMenuItem
|
||||
key="edit"
|
||||
onClick={async () => {
|
||||
// onOpen()
|
||||
await AgentSettingsPopup.show({
|
||||
agentId: agent.id
|
||||
})
|
||||
}}>
|
||||
<EditIcon size={14} />
|
||||
{t('common.edit')}
|
||||
</ContextMenuItem>
|
||||
<ContextMenuItem
|
||||
key="delete"
|
||||
className="text-danger"
|
||||
onClick={() => {
|
||||
window.modal.confirm({
|
||||
title: t('agent.delete.title'),
|
||||
content: t('agent.delete.content'),
|
||||
centered: true,
|
||||
okButtonProps: { danger: true },
|
||||
onOk: () => onDelete(agent)
|
||||
})
|
||||
}}>
|
||||
<DeleteIcon size={14} className="lucide-custom text-danger" />
|
||||
<span className="text-danger">{t('common.delete')}</span>
|
||||
</ContextMenuItem>
|
||||
</ContextMenuContent>
|
||||
</ContextMenu>
|
||||
)
|
||||
}
|
||||
|
||||
const ButtonContainer: React.FC<React.ComponentProps<typeof Button>> = ({ className, children, ...props }) => (
|
||||
<Button
|
||||
{...props}
|
||||
export const Container: React.FC<{ isActive?: boolean } & React.HTMLAttributes<HTMLDivElement>> = ({
|
||||
className,
|
||||
isActive,
|
||||
...props
|
||||
}) => (
|
||||
<div
|
||||
className={cn(
|
||||
'relative mb-2 flex h-[37px] flex-row justify-between p-2.5',
|
||||
'rounded-[var(--list-item-border-radius)]',
|
||||
'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',
|
||||
'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)]',
|
||||
isActive && 'bg-[var(--color-list-item)] shadow-[0_1px_2px_0_rgba(0,0,0,0.05)]',
|
||||
className
|
||||
)}>
|
||||
{children}
|
||||
</Button>
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
|
||||
const AssistantNameRow: React.FC<React.HTMLAttributes<HTMLDivElement>> = ({ className, ...props }) => (
|
||||
export const AssistantNameRow: React.FC<React.HTMLAttributes<HTMLDivElement>> = ({ className, ...props }) => (
|
||||
<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}
|
||||
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 EmojiIcon from '@renderer/components/EmojiIcon'
|
||||
import { CopyIcon, DeleteIcon, EditIcon } from '@renderer/components/Icons'
|
||||
@ -28,9 +29,8 @@ import {
|
||||
Tag,
|
||||
Tags
|
||||
} 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 styled from 'styled-components'
|
||||
import * as tinyPinyin from 'tiny-pinyin'
|
||||
|
||||
import AssistantTagsPopup from './AssistantTagsPopup'
|
||||
@ -46,6 +46,8 @@ interface AssistantItemProps {
|
||||
copyAssistant: (assistant: Assistant) => void
|
||||
onTagClick?: (tag: string) => void
|
||||
handleSortByChange?: (sortType: AssistantsSortType) => void
|
||||
sortByPinyinAsc?: () => void
|
||||
sortByPinyinDesc?: () => void
|
||||
}
|
||||
|
||||
const AssistantItem: FC<AssistantItemProps> = ({
|
||||
@ -56,7 +58,9 @@ const AssistantItem: FC<AssistantItemProps> = ({
|
||||
onDelete,
|
||||
addPreset,
|
||||
copyAssistant,
|
||||
handleSortByChange
|
||||
handleSortByChange,
|
||||
sortByPinyinAsc: externalSortByPinyinAsc,
|
||||
sortByPinyinDesc: externalSortByPinyinDesc
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const { allTags } = useTags()
|
||||
@ -78,14 +82,19 @@ const AssistantItem: FC<AssistantItemProps> = ({
|
||||
setIsPending(hasPending)
|
||||
}, [isActive, assistant.topics])
|
||||
|
||||
const sortByPinyinAsc = useCallback(() => {
|
||||
// Local sort functions
|
||||
const localSortByPinyinAsc = useCallback(() => {
|
||||
updateAssistants(sortAssistantsByPinyin(assistants, true))
|
||||
}, [assistants, updateAssistants])
|
||||
|
||||
const sortByPinyinDesc = useCallback(() => {
|
||||
const localSortByPinyinDesc = useCallback(() => {
|
||||
updateAssistants(sortAssistantsByPinyin(assistants, false))
|
||||
}, [assistants, updateAssistants])
|
||||
|
||||
// Use external sort functions if provided, otherwise use local ones
|
||||
const sortByPinyinAsc = externalSortByPinyinAsc || localSortByPinyinAsc
|
||||
const sortByPinyinDesc = externalSortByPinyinDesc || localSortByPinyinDesc
|
||||
|
||||
const menuItems = useMemo(
|
||||
() =>
|
||||
getMenuItems({
|
||||
@ -145,7 +154,7 @@ const AssistantItem: FC<AssistantItemProps> = ({
|
||||
menu={{ items: menuItems }}
|
||||
trigger={['contextMenu']}
|
||||
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}>
|
||||
{assistantIconType === 'model' ? (
|
||||
<ModelAvatar
|
||||
@ -380,65 +389,75 @@ function getMenuItems({
|
||||
]
|
||||
}
|
||||
|
||||
const Container = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
padding: 0 8px;
|
||||
height: 37px;
|
||||
position: relative;
|
||||
border-radius: var(--list-item-border-radius);
|
||||
border: 0.5px solid transparent;
|
||||
width: calc(var(--assistants-width) - 20px);
|
||||
cursor: pointer;
|
||||
const Container = ({
|
||||
children,
|
||||
isActive,
|
||||
className,
|
||||
...props
|
||||
}: PropsWithChildren<{ isActive?: boolean } & React.HTMLAttributes<HTMLDivElement>>) => (
|
||||
<div
|
||||
{...props}
|
||||
className={cn(
|
||||
'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)]',
|
||||
isActive && 'bg-[var(--color-list-item)] shadow-[0_1px_2px_0_rgba(0,0,0,0.05)]',
|
||||
className
|
||||
)}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
|
||||
&:hover {
|
||||
background-color: var(--color-list-item-hover);
|
||||
}
|
||||
&.active {
|
||||
background-color: var(--color-list-item);
|
||||
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
`
|
||||
const AssistantNameRow = ({
|
||||
children,
|
||||
className,
|
||||
...props
|
||||
}: PropsWithChildren<{} & React.HTMLAttributes<HTMLDivElement>>) => (
|
||||
<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`
|
||||
color: var(--color-text);
|
||||
font-size: 13px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
`
|
||||
const AssistantName = ({
|
||||
children,
|
||||
className,
|
||||
...props
|
||||
}: PropsWithChildren<{} & React.HTMLAttributes<HTMLDivElement>>) => (
|
||||
<div
|
||||
{...props}
|
||||
className={cn('min-w-0 flex-1 overflow-hidden text-ellipsis whitespace-nowrap text-[13px]', className)}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
|
||||
const AssistantName = styled.div`
|
||||
font-size: 13px;
|
||||
`
|
||||
const MenuButton = ({
|
||||
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`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-width: 22px;
|
||||
height: 22px;
|
||||
min-height: 22px;
|
||||
border-radius: 11px;
|
||||
position: absolute;
|
||||
background-color: var(--color-background);
|
||||
right: 9px;
|
||||
top: 6px;
|
||||
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;
|
||||
`
|
||||
const TopicCount = ({
|
||||
children,
|
||||
className,
|
||||
...props
|
||||
}: PropsWithChildren<{} & React.HTMLAttributes<HTMLDivElement>>) => (
|
||||
<div
|
||||
{...props}
|
||||
className={cn(
|
||||
'flex flex-row items-center justify-center rounded-[10px] text-[10px] text-[var(--color-text)]',
|
||||
className
|
||||
)}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
|
||||
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 { useAgent } from '@renderer/hooks/agents/useAgent'
|
||||
import { useSessions } from '@renderer/hooks/agents/useSessions'
|
||||
@ -13,10 +13,10 @@ import {
|
||||
import { CreateSessionForm } from '@renderer/types'
|
||||
import { buildAgentSessionTopicId } from '@renderer/utils/agentSession'
|
||||
import { AnimatePresence, motion } from 'framer-motion'
|
||||
import { Plus } from 'lucide-react'
|
||||
import { memo, useCallback, useEffect } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import AddButton from './AddButton'
|
||||
import SessionItem from './SessionItem'
|
||||
|
||||
// const logger = loggerService.withContext('SessionsTab')
|
||||
@ -115,12 +115,9 @@ const Sessions: React.FC<SessionsProps> = ({ agentId }) => {
|
||||
transition={{ duration: 0.3 }}
|
||||
className="sessions-tab flex h-full w-full flex-col p-2">
|
||||
<motion.div initial={{ opacity: 0, y: -10 }} animate={{ opacity: 1, y: 0 }}>
|
||||
<Button
|
||||
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" />
|
||||
<AddButton onPress={handleCreateSession} className="mb-2">
|
||||
{t('agent.session.add.title')}
|
||||
</Button>
|
||||
</AddButton>
|
||||
</motion.div>
|
||||
<AnimatePresence>
|
||||
{/* 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,
|
||||
PinIcon,
|
||||
PinOffIcon,
|
||||
PlusIcon,
|
||||
Save,
|
||||
Sparkles,
|
||||
UploadIcon,
|
||||
@ -52,6 +51,8 @@ import { useTranslation } from 'react-i18next'
|
||||
import { useDispatch, useSelector } from 'react-redux'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import AddButton from './AddButton'
|
||||
|
||||
interface Props {
|
||||
assistant: Assistant
|
||||
activeTopic: Topic
|
||||
@ -497,13 +498,12 @@ export const Topics: React.FC<Props> = ({ assistant: _assistant, activeTopic, se
|
||||
className="topics-tab"
|
||||
list={sortedTopics}
|
||||
onUpdate={updateTopics}
|
||||
style={{ height: '100%', padding: '13px 0 10px 10px' }}
|
||||
style={{ height: '100%', padding: '11px 0 10px 10px' }}
|
||||
itemContainerStyle={{ paddingBottom: '8px' }}
|
||||
header={
|
||||
<AddTopicButton onClick={() => EventEmitter.emit(EVENT_NAMES.ADD_NEW_TOPIC)}>
|
||||
<PlusIcon size={16} />
|
||||
<AddButton onPress={() => EventEmitter.emit(EVENT_NAMES.ADD_NEW_TOPIC)} className="mb-2">
|
||||
{t('chat.add.topic.title')}
|
||||
</AddTopicButton>
|
||||
</AddButton>
|
||||
}>
|
||||
{(topic) => {
|
||||
const isActive = topic.id === activeTopic?.id
|
||||
@ -740,31 +740,6 @@ const FulfilledIndicator = styled.div.attrs({
|
||||
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`
|
||||
color: var(--color-text-2);
|
||||
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[]
|
||||
collapsedTags: Record<string, boolean>
|
||||
presets: AssistantPreset[]
|
||||
unifiedListOrder: Array<{ type: 'agent' | 'assistant'; id: string }>
|
||||
}
|
||||
|
||||
const initialState: AssistantsState = {
|
||||
@ -20,7 +21,8 @@ const initialState: AssistantsState = {
|
||||
assistants: [getDefaultAssistant()],
|
||||
tagsOrder: [],
|
||||
collapsedTags: {},
|
||||
presets: []
|
||||
presets: [],
|
||||
unifiedListOrder: []
|
||||
}
|
||||
|
||||
const assistantsSlice = createSlice({
|
||||
@ -96,6 +98,9 @@ const assistantsSlice = createSlice({
|
||||
[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 }>) => {
|
||||
const topic = action.payload.topic
|
||||
topic.createdAt = topic.createdAt || new Date().toISOString()
|
||||
@ -244,6 +249,7 @@ export const {
|
||||
setTagsOrder,
|
||||
updateAssistantSettings,
|
||||
updateTagCollapse,
|
||||
setUnifiedListOrder,
|
||||
setAssistantPresets,
|
||||
addAssistantPreset,
|
||||
removeAssistantPreset,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user