mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2026-01-08 06:19:05 +08:00
feat(agents): add agent management with delete confirmation
- Add agent deletion confirmation dialog with translations - Implement agent list display and drag-and-drop functionality - Include avatar support for agent types - Add success message for agent deletion
This commit is contained in:
parent
941a6666e6
commit
9a71a01b66
@ -20,7 +20,7 @@ import ClaudeIcon from '@renderer/assets/images/models/claude.png'
|
|||||||
import { useAgents } from '@renderer/hooks/useAgents'
|
import { useAgents } from '@renderer/hooks/useAgents'
|
||||||
import { useTimer } from '@renderer/hooks/useTimer'
|
import { useTimer } from '@renderer/hooks/useTimer'
|
||||||
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
|
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
|
||||||
import { AgentEntity, isAgentType } from '@renderer/types'
|
import { AgentEntity, AgentType, isAgentType } from '@renderer/types'
|
||||||
import { uuid } from '@renderer/utils'
|
import { uuid } from '@renderer/utils'
|
||||||
import { Plus } from 'lucide-react'
|
import { Plus } from 'lucide-react'
|
||||||
import { ChangeEvent, FormEvent, useCallback, useMemo, useRef, useState } from 'react'
|
import { ChangeEvent, FormEvent, useCallback, useMemo, useRef, useState } from 'react'
|
||||||
@ -204,7 +204,8 @@ export const AddAgentModal: React.FC = () => {
|
|||||||
instructions: form.instructions,
|
instructions: form.instructions,
|
||||||
created_at: new Date().toISOString(),
|
created_at: new Date().toISOString(),
|
||||||
updated_at: new Date().toISOString(),
|
updated_at: new Date().toISOString(),
|
||||||
model: form.model
|
model: form.model,
|
||||||
|
avatar: getAvatar(form.type)
|
||||||
} satisfies AgentEntity
|
} satisfies AgentEntity
|
||||||
logger.debug('Agent', agent)
|
logger.debug('Agent', agent)
|
||||||
addAgent(agent)
|
addAgent(agent)
|
||||||
@ -282,3 +283,11 @@ export const AddAgentModal: React.FC = () => {
|
|||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getAvatar = (type: AgentType) => {
|
||||||
|
switch (type) {
|
||||||
|
case 'claude-code':
|
||||||
|
return ClaudeIcon
|
||||||
|
}
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|||||||
@ -1,9 +1,10 @@
|
|||||||
import { useAppDispatch } from '@renderer/store'
|
import { useAppDispatch, useAppSelector } from '@renderer/store'
|
||||||
import { addAgent, removeAgent, setAgents, updateAgent } from '@renderer/store/agents'
|
import { addAgent, removeAgent, setAgents, updateAgent } from '@renderer/store/agents'
|
||||||
import { AgentEntity } from '@renderer/types'
|
import { AgentEntity } from '@renderer/types'
|
||||||
import { useCallback } from 'react'
|
import { useCallback } from 'react'
|
||||||
|
|
||||||
export const useAgents = () => {
|
export const useAgents = () => {
|
||||||
|
const agents = useAppSelector((state) => state.agents.agentsNew)
|
||||||
const dispatch = useAppDispatch()
|
const dispatch = useAppDispatch()
|
||||||
/**
|
/**
|
||||||
* Adds a new agent to the store
|
* Adds a new agent to the store
|
||||||
@ -50,6 +51,7 @@ export const useAgents = () => {
|
|||||||
)
|
)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
agents,
|
||||||
addAgent: addAgent_,
|
addAgent: addAgent_,
|
||||||
removeAgent: removeAgent_,
|
removeAgent: removeAgent_,
|
||||||
updateAgent: updateAgent_,
|
updateAgent: updateAgent_,
|
||||||
|
|||||||
@ -9,6 +9,13 @@
|
|||||||
"label": "Agent Type",
|
"label": "Agent Type",
|
||||||
"placeholder": "Select an agent type"
|
"placeholder": "Select an agent type"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"delete": {
|
||||||
|
"content": "Deleting the agent will forcibly stop and delete all sessions associated with the agent. Are you sure?",
|
||||||
|
"title": "Delete Agent"
|
||||||
|
},
|
||||||
|
"edit": {
|
||||||
|
"title": "Edit Agent"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"agents": {
|
"agents": {
|
||||||
@ -763,6 +770,7 @@
|
|||||||
"default": "Default",
|
"default": "Default",
|
||||||
"delete": "Delete",
|
"delete": "Delete",
|
||||||
"delete_confirm": "Are you sure you want to delete?",
|
"delete_confirm": "Are you sure you want to delete?",
|
||||||
|
"delete_success": "Deleted successfully",
|
||||||
"description": "Description",
|
"description": "Description",
|
||||||
"detail": "Detail",
|
"detail": "Detail",
|
||||||
"disabled": "Disabled",
|
"disabled": "Disabled",
|
||||||
|
|||||||
@ -9,6 +9,10 @@
|
|||||||
"label": "Agent 类型",
|
"label": "Agent 类型",
|
||||||
"placeholder": "选择 Agent 类型"
|
"placeholder": "选择 Agent 类型"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"delete": {
|
||||||
|
"content": "删除该 Agent 将强制终止并删除该 Agent 下的所有会话。您确定吗?",
|
||||||
|
"title": "删除 Agent"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"agents": {
|
"agents": {
|
||||||
@ -763,6 +767,7 @@
|
|||||||
"default": "默认",
|
"default": "默认",
|
||||||
"delete": "删除",
|
"delete": "删除",
|
||||||
"delete_confirm": "确定要删除吗?",
|
"delete_confirm": "确定要删除吗?",
|
||||||
|
"delete_success": "删除成功",
|
||||||
"description": "描述",
|
"description": "描述",
|
||||||
"detail": "详情",
|
"detail": "详情",
|
||||||
"disabled": "已禁用",
|
"disabled": "已禁用",
|
||||||
|
|||||||
@ -1,19 +1,21 @@
|
|||||||
import { DownOutlined, RightOutlined } from '@ant-design/icons'
|
import { DownOutlined, RightOutlined } from '@ant-design/icons'
|
||||||
import { Button } from '@heroui/react'
|
import { Button, Divider } from '@heroui/react'
|
||||||
import { DraggableList } from '@renderer/components/DraggableList'
|
import { DraggableList } from '@renderer/components/DraggableList'
|
||||||
import { AddAgentModal } from '@renderer/components/Popups/AddAgentModal'
|
import { AddAgentModal } from '@renderer/components/Popups/AddAgentModal'
|
||||||
import Scrollbar from '@renderer/components/Scrollbar'
|
import Scrollbar from '@renderer/components/Scrollbar'
|
||||||
|
import { useAgents } from '@renderer/hooks/useAgents'
|
||||||
import { useAssistants } from '@renderer/hooks/useAssistant'
|
import { useAssistants } from '@renderer/hooks/useAssistant'
|
||||||
import { useAssistantPresets } from '@renderer/hooks/useAssistantPresets'
|
import { useAssistantPresets } from '@renderer/hooks/useAssistantPresets'
|
||||||
import { useAssistantsTabSortType } from '@renderer/hooks/useStore'
|
import { useAssistantsTabSortType } from '@renderer/hooks/useStore'
|
||||||
import { useTags } from '@renderer/hooks/useTags'
|
import { useTags } from '@renderer/hooks/useTags'
|
||||||
import { Assistant, AssistantsSortType } from '@renderer/types'
|
import { AgentEntity, Assistant, AssistantsSortType } from '@renderer/types'
|
||||||
import { Tooltip } from 'antd'
|
import { Tooltip } from 'antd'
|
||||||
import { Plus } from 'lucide-react'
|
import { Plus } from 'lucide-react'
|
||||||
import { FC, useCallback, useMemo, useRef, useState } from 'react'
|
import { FC, useCallback, useMemo, useRef, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
|
|
||||||
|
import AgentItem from './components/AgentItem'
|
||||||
import AssistantItem from './components/AssistantItem'
|
import AssistantItem from './components/AssistantItem'
|
||||||
|
|
||||||
interface AssistantsTabProps {
|
interface AssistantsTabProps {
|
||||||
@ -35,6 +37,7 @@ const Assistants: FC<AssistantsTabProps> = ({
|
|||||||
const { getGroupedAssistants, collapsedTags, toggleTagCollapse } = useTags()
|
const { getGroupedAssistants, collapsedTags, toggleTagCollapse } = useTags()
|
||||||
const { assistantsTabSortType = 'list', setAssistantsTabSortType } = useAssistantsTabSortType()
|
const { assistantsTabSortType = 'list', setAssistantsTabSortType } = useAssistantsTabSortType()
|
||||||
const containerRef = useRef<HTMLDivElement>(null)
|
const containerRef = useRef<HTMLDivElement>(null)
|
||||||
|
const { agents, setAgents, removeAgent } = useAgents()
|
||||||
|
|
||||||
const onDelete = useCallback(
|
const onDelete = useCallback(
|
||||||
(assistant: Assistant) => {
|
(assistant: Assistant) => {
|
||||||
@ -48,6 +51,14 @@ const Assistants: FC<AssistantsTabProps> = ({
|
|||||||
[activeAssistant, assistants, removeAssistant, setActiveAssistant, onCreateDefaultAssistant]
|
[activeAssistant, assistants, removeAssistant, setActiveAssistant, onCreateDefaultAssistant]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const onDeleteAgent = useCallback(
|
||||||
|
(agent: AgentEntity) => {
|
||||||
|
removeAgent(agent.id)
|
||||||
|
window.toast.success(t('common.delete_success'))
|
||||||
|
},
|
||||||
|
[removeAgent, t]
|
||||||
|
)
|
||||||
|
|
||||||
const handleSortByChange = useCallback(
|
const handleSortByChange = useCallback(
|
||||||
(sortType: AssistantsSortType) => {
|
(sortType: AssistantsSortType) => {
|
||||||
setAssistantsTabSortType(sortType)
|
setAssistantsTabSortType(sortType)
|
||||||
@ -81,19 +92,6 @@ const Assistants: FC<AssistantsTabProps> = ({
|
|||||||
)
|
)
|
||||||
}, [onCreateAssistant, t])
|
}, [onCreateAssistant, t])
|
||||||
|
|
||||||
// const AddAgentButton = useCallback(() => {
|
|
||||||
// return (
|
|
||||||
// <AssistantAddItem onClick={onCreateAgent}>
|
|
||||||
// <AddItemWrapper>
|
|
||||||
// <Plus size={16} style={{ marginRight: 4, flexShrink: 0 }} />
|
|
||||||
// <Typography.Text style={{ color: 'inherit' }} ellipsis={{ tooltip: t('agent.add.title') }}>
|
|
||||||
// {t('agent.add.title')}
|
|
||||||
// </Typography.Text>
|
|
||||||
// </AddItemWrapper>
|
|
||||||
// </AssistantAddItem>
|
|
||||||
// )
|
|
||||||
// }, [onCreateAgent, t])
|
|
||||||
|
|
||||||
if (assistantsTabSortType === 'tags') {
|
if (assistantsTabSortType === 'tags') {
|
||||||
return (
|
return (
|
||||||
<Container className="assistants-tab" ref={containerRef}>
|
<Container className="assistants-tab" ref={containerRef}>
|
||||||
@ -150,6 +148,15 @@ const Assistants: FC<AssistantsTabProps> = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Container className="assistants-tab" ref={containerRef}>
|
<Container className="assistants-tab" ref={containerRef}>
|
||||||
|
<DraggableList
|
||||||
|
list={agents}
|
||||||
|
onUpdate={setAgents}
|
||||||
|
onDragStart={() => setDragging(true)}
|
||||||
|
onDragEnd={() => setDragging(false)}>
|
||||||
|
{(agent) => <AgentItem agent={agent} isActive={false} onDelete={onDeleteAgent} />}
|
||||||
|
</DraggableList>
|
||||||
|
{!dragging && <AddAgentModal />}
|
||||||
|
<Divider className="my-2" />
|
||||||
<DraggableList
|
<DraggableList
|
||||||
list={assistants}
|
list={assistants}
|
||||||
onUpdate={updateAssistants}
|
onUpdate={updateAssistants}
|
||||||
@ -171,8 +178,6 @@ const Assistants: FC<AssistantsTabProps> = ({
|
|||||||
)}
|
)}
|
||||||
</DraggableList>
|
</DraggableList>
|
||||||
{!dragging && renderAddAssistantButton}
|
{!dragging && renderAddAssistantButton}
|
||||||
{/* {!dragging && <AddAgentButton />} */}
|
|
||||||
{!dragging && <AddAgentModal />}
|
|
||||||
<div style={{ minHeight: 10 }}></div>
|
<div style={{ minHeight: 10 }}></div>
|
||||||
</Container>
|
</Container>
|
||||||
)
|
)
|
||||||
|
|||||||
134
src/renderer/src/pages/home/Tabs/components/AgentItem.tsx
Normal file
134
src/renderer/src/pages/home/Tabs/components/AgentItem.tsx
Normal file
@ -0,0 +1,134 @@
|
|||||||
|
import { Avatar, cn } from '@heroui/react'
|
||||||
|
import { loggerService } from '@logger'
|
||||||
|
import { DeleteIcon, EditIcon } from '@renderer/components/Icons'
|
||||||
|
import { AgentEntity } from '@renderer/types'
|
||||||
|
import { Dropdown, MenuProps } from 'antd'
|
||||||
|
import { FC, memo, useCallback, useMemo } from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
|
const logger = loggerService.withContext('AgentItem')
|
||||||
|
|
||||||
|
interface AgentItemProps {
|
||||||
|
agent: AgentEntity
|
||||||
|
isActive: boolean
|
||||||
|
onDelete: (agent: AgentEntity) => void
|
||||||
|
onTagClick?: (tag: string) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const AgentItem: FC<AgentItemProps> = ({ agent, isActive, onDelete }) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
// const { agents } = useAgents()
|
||||||
|
|
||||||
|
const menuItems = useMemo(
|
||||||
|
() =>
|
||||||
|
getMenuItems({
|
||||||
|
agent,
|
||||||
|
t,
|
||||||
|
onDelete
|
||||||
|
}),
|
||||||
|
[agent, t, onDelete]
|
||||||
|
)
|
||||||
|
|
||||||
|
const AgentLabel = useCallback(() => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{agent.avatar && <Avatar className="h-6 w-6" src={agent.avatar} />}
|
||||||
|
<span className="text-sm">{agent.name}</span>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}, [agent.avatar, agent.name])
|
||||||
|
|
||||||
|
const handleClick = () => logger.debug('not implemented')
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dropdown
|
||||||
|
menu={{ items: menuItems }}
|
||||||
|
trigger={['contextMenu']}
|
||||||
|
popupRender={(menu) => <div onPointerDown={(e) => e.stopPropagation()}>{menu}</div>}>
|
||||||
|
<Container onClick={handleClick} className={isActive ? 'active' : ''}>
|
||||||
|
<AssistantNameRow className="name" title={agent.name}>
|
||||||
|
<AgentLabel />
|
||||||
|
</AssistantNameRow>
|
||||||
|
</Container>
|
||||||
|
</Dropdown>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提取创建菜单配置的函数
|
||||||
|
function getMenuItems({ agent, t, onDelete }): MenuProps['items'] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
label: t('common.edit'),
|
||||||
|
key: 'edit',
|
||||||
|
icon: <EditIcon size={14} />,
|
||||||
|
onClick: () => window.toast.info('not implemented')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t('common.delete'),
|
||||||
|
key: 'delete',
|
||||||
|
icon: <DeleteIcon size={14} className="lucide-custom" />,
|
||||||
|
danger: true,
|
||||||
|
onClick: () => {
|
||||||
|
window.modal.confirm({
|
||||||
|
title: t('agent.delete.title'),
|
||||||
|
content: t('agent.delete.content'),
|
||||||
|
centered: true,
|
||||||
|
okButtonProps: { danger: true },
|
||||||
|
onOk: () => onDelete(agent)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
const Container: React.FC<React.HTMLAttributes<HTMLDivElement>> = ({ className, ...props }) => (
|
||||||
|
<div
|
||||||
|
{...props}
|
||||||
|
className={cn(
|
||||||
|
'relative flex h-[37px] flex-row justify-between p-2',
|
||||||
|
'rounded-[var(--list-item-border-radius)]',
|
||||||
|
'border-[0.5px] border-transparent',
|
||||||
|
'w-[calc(var(--assistants-width)_-_20px)]',
|
||||||
|
'hover:bg-[var(--color-list-item-hover)]',
|
||||||
|
className?.includes('active') && 'bg-[var(--color-list-item)] shadow-sm',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
|
||||||
|
const AssistantNameRow: React.FC<React.HTMLAttributes<HTMLDivElement>> = ({ className, ...props }) => (
|
||||||
|
<div
|
||||||
|
{...props}
|
||||||
|
className={cn('text-[13px] text-[var(--color-text)]', 'flex flex-row items-center gap-2', className)}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
|
||||||
|
// const MenuButton: React.FC<React.HTMLAttributes<HTMLDivElement>> = ({ className, ...props }) => (
|
||||||
|
// <div
|
||||||
|
// {...props}
|
||||||
|
// className={cn(
|
||||||
|
// 'flex flex-row items-center justify-center',
|
||||||
|
// 'h-[22px] min-h-[22px] min-w-[22px]',
|
||||||
|
// 'absolute rounded-[11px]',
|
||||||
|
// 'bg-[var(--color-background)]',
|
||||||
|
// 'top-[6px] right-[9px]',
|
||||||
|
// 'px-[5px]',
|
||||||
|
// 'border-[0.5px] border-[var(--color-border)]',
|
||||||
|
// className
|
||||||
|
// )}
|
||||||
|
// />
|
||||||
|
// )
|
||||||
|
|
||||||
|
// const TopicCount: React.FC<React.HTMLAttributes<HTMLDivElement>> = ({ className, ...props }) => (
|
||||||
|
// <div
|
||||||
|
// {...props}
|
||||||
|
// className={cn(
|
||||||
|
// 'text-[10px] text-[var(--color-text)]',
|
||||||
|
// 'rounded-[10px]',
|
||||||
|
// 'flex flex-row items-center justify-center',
|
||||||
|
// className
|
||||||
|
// )}
|
||||||
|
// />
|
||||||
|
// )
|
||||||
|
|
||||||
|
export default memo(AgentItem)
|
||||||
Loading…
Reference in New Issue
Block a user