mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2026-01-04 03:40:33 +08:00
feat(agents): add edit functionality and update translations
- Refactor AddAgentModal into AgentModal to support both add and edit operations - Add edit button to AgentItem with corresponding modal functionality - Update translations for edit and update success messages
This commit is contained in:
parent
df31629c5f
commit
5f9c2d7f6a
@ -1,6 +1,7 @@
|
||||
import {
|
||||
Avatar,
|
||||
Button,
|
||||
cn,
|
||||
Form,
|
||||
Input,
|
||||
Modal,
|
||||
@ -22,8 +23,7 @@ import { useTimer } from '@renderer/hooks/useTimer'
|
||||
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
|
||||
import { AgentEntity, AgentType, isAgentType } from '@renderer/types'
|
||||
import { uuid } from '@renderer/utils'
|
||||
import { Plus } from 'lucide-react'
|
||||
import { ChangeEvent, FormEvent, useCallback, useMemo, useRef, useState } from 'react'
|
||||
import { ChangeEvent, FormEvent, ReactNode, useCallback, useMemo, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import { ErrorBoundary } from '../ErrorBoundary'
|
||||
@ -52,19 +52,52 @@ type AgentForm = {
|
||||
model?: AgentEntity['model']
|
||||
}
|
||||
|
||||
export const AddAgentModal: React.FC = () => {
|
||||
const { isOpen, onClose, onOpen } = useDisclosure()
|
||||
interface BaseProps {
|
||||
agent?: AgentEntity
|
||||
}
|
||||
|
||||
interface TriggerProps extends BaseProps {
|
||||
trigger: { content: ReactNode; className?: string }
|
||||
isOpen?: never
|
||||
onClose?: never
|
||||
}
|
||||
|
||||
interface StateProps extends BaseProps {
|
||||
trigger?: never
|
||||
isOpen: boolean
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
type Props = TriggerProps | StateProps
|
||||
|
||||
/**
|
||||
* Modal component for creating or editing an agent.
|
||||
*
|
||||
* Either trigger or isOpen and onClose is given.
|
||||
* @param agent - Optional agent entity for editing mode.
|
||||
* @param trigger - Optional trigger element that opens the modal. It MUST propagate the click event to trigger the modal.
|
||||
* @param isOpen - Optional controlled modal open state. From useDisclosure.
|
||||
* @param onClose - Optional callback when modal closes. From useDisclosure.
|
||||
* @returns Modal component for agent creation/editing
|
||||
*/
|
||||
export const AgentModal: React.FC<Props> = ({ agent, trigger, isOpen: _isOpen, onClose: _onClose }) => {
|
||||
const { isOpen, onClose, onOpen } = useDisclosure({ isOpen: _isOpen, onClose: _onClose })
|
||||
const { t } = useTranslation()
|
||||
const loadingRef = useRef(false)
|
||||
const { setTimeoutTimer } = useTimer()
|
||||
const { addAgent } = useAgents()
|
||||
const { addAgent, updateAgent } = useAgents()
|
||||
const isEditing = (agent?: AgentEntity) => agent !== undefined
|
||||
|
||||
// default values. may change to undefined.
|
||||
const [form, setForm] = useState<AgentForm>({
|
||||
type: 'claude-code',
|
||||
name: 'Claude Code',
|
||||
model: 'claude-4-sonnet'
|
||||
})
|
||||
const [form, setForm] = useState<AgentForm>(
|
||||
isEditing(agent)
|
||||
? agent
|
||||
: {
|
||||
type: 'claude-code',
|
||||
name: 'Claude Code',
|
||||
model: 'claude-4-sonnet'
|
||||
}
|
||||
)
|
||||
|
||||
const Option = useCallback(
|
||||
({ option }: { option?: Option | null }) => {
|
||||
@ -196,46 +229,86 @@ export const AddAgentModal: React.FC = () => {
|
||||
return
|
||||
}
|
||||
|
||||
const agent = {
|
||||
id: uuid(),
|
||||
type: form.type,
|
||||
name: form.name,
|
||||
description: form.description,
|
||||
instructions: form.instructions,
|
||||
created_at: new Date().toISOString(),
|
||||
updated_at: new Date().toISOString(),
|
||||
model: form.model,
|
||||
avatar: getAvatar(form.type)
|
||||
} satisfies AgentEntity
|
||||
logger.debug('Agent', agent)
|
||||
addAgent(agent)
|
||||
window.toast.success(t('common.add_success'))
|
||||
let _agent: AgentEntity
|
||||
if (isEditing(agent)) {
|
||||
_agent = {
|
||||
...agent,
|
||||
// type: form.type,
|
||||
name: form.name,
|
||||
description: form.description,
|
||||
instructions: form.instructions,
|
||||
updated_at: new Date().toISOString(),
|
||||
model: form.model
|
||||
// avatar: getAvatar(form.type)
|
||||
} satisfies AgentEntity
|
||||
updateAgent(_agent)
|
||||
window.toast.success(t('common.update_success'))
|
||||
} else {
|
||||
_agent = {
|
||||
id: uuid(),
|
||||
type: form.type,
|
||||
name: form.name,
|
||||
description: form.description,
|
||||
instructions: form.instructions,
|
||||
created_at: new Date().toISOString(),
|
||||
updated_at: new Date().toISOString(),
|
||||
model: form.model,
|
||||
avatar: getAvatar(form.type)
|
||||
} satisfies AgentEntity
|
||||
addAgent(_agent)
|
||||
window.toast.success(t('common.add_success'))
|
||||
}
|
||||
|
||||
logger.debug('Agent', _agent)
|
||||
loadingRef.current = false
|
||||
|
||||
setTimeoutTimer('onCreateAgent', () => EventEmitter.emit(EVENT_NAMES.SHOW_ASSISTANTS), 0)
|
||||
onClose()
|
||||
},
|
||||
[form.type, form.model, form.name, form.description, form.instructions, addAgent, t, setTimeoutTimer, onClose]
|
||||
[
|
||||
form.type,
|
||||
form.model,
|
||||
form.name,
|
||||
form.description,
|
||||
form.instructions,
|
||||
agent,
|
||||
setTimeoutTimer,
|
||||
onClose,
|
||||
t,
|
||||
updateAgent,
|
||||
addAgent
|
||||
]
|
||||
)
|
||||
|
||||
return (
|
||||
<ErrorBoundary>
|
||||
{/* NOTE: Hero UI Modal Pattern: Combine the Button and Modal components into a single
|
||||
encapsulated component. This is because the Modal component needs to bind the onOpen
|
||||
event handler to the Button for proper focus management. */}
|
||||
<Button onPress={onOpen} className="justify-start bg-transparent text-foreground-500 hover:bg-accent">
|
||||
<Plus size={16} style={{ marginRight: 4, flexShrink: 0 }} />
|
||||
{t('agent.add.title')}
|
||||
</Button>
|
||||
event handler to the Button for proper focus management.
|
||||
|
||||
Or just use external isOpen/onOpen/onClose to control modal state.
|
||||
*/}
|
||||
|
||||
{trigger && (
|
||||
<div
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
onOpen()
|
||||
}}
|
||||
className={cn('w-full', trigger.className)}>
|
||||
{trigger.content}
|
||||
</div>
|
||||
)}
|
||||
<Modal isOpen={isOpen} onClose={onClose}>
|
||||
<ModalContent>
|
||||
{(onClose) => (
|
||||
<>
|
||||
<ModalHeader>{t('agent.add.title')}</ModalHeader>
|
||||
<ModalHeader>{isEditing(agent) ? t('agent.edit.title') : t('agent.add.title')}</ModalHeader>
|
||||
<Form onSubmit={onSubmit} className="w-full">
|
||||
<ModalBody className="w-full">
|
||||
<Select
|
||||
isRequired
|
||||
isDisabled={isEditing(agent)}
|
||||
selectionMode="single"
|
||||
selectedKeys={[form.type]}
|
||||
onChange={onAgentTypeChange}
|
||||
@ -272,7 +345,7 @@ export const AddAgentModal: React.FC = () => {
|
||||
<ModalFooter className="w-full">
|
||||
<Button onPress={onClose}>{t('common.close')}</Button>
|
||||
<Button color="primary" type="submit" isLoading={loadingRef.current}>
|
||||
{t('common.add')}
|
||||
{isEditing(agent) ? t('common.confirm') : t('common.add')}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</Form>
|
||||
@ -833,6 +833,7 @@
|
||||
"success": "Success",
|
||||
"swap": "Swap",
|
||||
"topics": "Topics",
|
||||
"update_success": "Update successfully",
|
||||
"upload_files": "Upload file",
|
||||
"warning": "Warning",
|
||||
"you": "You"
|
||||
|
||||
@ -13,6 +13,9 @@
|
||||
"delete": {
|
||||
"content": "删除该 Agent 将强制终止并删除该 Agent 下的所有会话。您确定吗?",
|
||||
"title": "删除 Agent"
|
||||
},
|
||||
"edit": {
|
||||
"title": "编辑 Agent"
|
||||
}
|
||||
},
|
||||
"agents": {
|
||||
@ -833,6 +836,7 @@
|
||||
"success": "成功",
|
||||
"swap": "交换",
|
||||
"topics": "话题",
|
||||
"update_success": "更新成功",
|
||||
"upload_files": "上传文件",
|
||||
"warning": "警告",
|
||||
"you": "用户"
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { DownOutlined, RightOutlined } from '@ant-design/icons'
|
||||
import { Button, Divider } from '@heroui/react'
|
||||
import { DraggableList } from '@renderer/components/DraggableList'
|
||||
import { AddAgentModal } from '@renderer/components/Popups/AddAgentModal'
|
||||
import { AgentModal } from '@renderer/components/Popups/AgentModal'
|
||||
import Scrollbar from '@renderer/components/Scrollbar'
|
||||
import { useAgents } from '@renderer/hooks/useAgents'
|
||||
import { useAssistants } from '@renderer/hooks/useAssistant'
|
||||
@ -155,7 +155,20 @@ const Assistants: FC<AssistantsTabProps> = ({
|
||||
onDragEnd={() => setDragging(false)}>
|
||||
{(agent) => <AgentItem agent={agent} isActive={false} onDelete={onDeleteAgent} />}
|
||||
</DraggableList>
|
||||
{!dragging && <AddAgentModal />}
|
||||
{!dragging && (
|
||||
<AgentModal
|
||||
trigger={{
|
||||
content: (
|
||||
<Button
|
||||
onPress={(e) => e.continuePropagation()}
|
||||
className="w-full justify-start bg-transparent text-foreground-500 hover:bg-accent">
|
||||
<Plus size={16} className="mr-1 shrink-0" />
|
||||
{t('agent.add.title')}
|
||||
</Button>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<Divider className="my-2" />
|
||||
<DraggableList
|
||||
list={assistants}
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import { Avatar, cn } from '@heroui/react'
|
||||
import { Avatar, cn, useDisclosure } from '@heroui/react'
|
||||
import { loggerService } from '@logger'
|
||||
import { DeleteIcon, EditIcon } from '@renderer/components/Icons'
|
||||
import { AgentModal } from '@renderer/components/Popups/AgentModal'
|
||||
import { AgentEntity } from '@renderer/types'
|
||||
import { ContextMenu, ContextMenuContent, ContextMenuItem, ContextMenuTrigger } from '@renderer/ui/context-menu'
|
||||
import { FC, memo, useCallback } from 'react'
|
||||
@ -17,6 +18,7 @@ interface AgentItemProps {
|
||||
|
||||
const AgentItem: FC<AgentItemProps> = ({ agent, isActive, onDelete }) => {
|
||||
const { t } = useTranslation()
|
||||
const { isOpen, onOpen, onClose } = useDisclosure()
|
||||
// const { agents } = useAgents()
|
||||
|
||||
const AgentLabel = useCallback(() => {
|
||||
@ -31,36 +33,43 @@ const AgentItem: FC<AgentItemProps> = ({ agent, isActive, onDelete }) => {
|
||||
const handleClick = () => logger.debug('not implemented')
|
||||
|
||||
return (
|
||||
<ContextMenu>
|
||||
<ContextMenuTrigger>
|
||||
<Container onClick={handleClick} className={isActive ? 'active' : ''}>
|
||||
<AssistantNameRow className="name" title={agent.name}>
|
||||
<AgentLabel />
|
||||
</AssistantNameRow>
|
||||
</Container>
|
||||
</ContextMenuTrigger>
|
||||
<ContextMenuContent>
|
||||
<ContextMenuItem key="edit" onClick={() => window.toast.info('not implemented')}>
|
||||
<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>
|
||||
<>
|
||||
<ContextMenu modal={false}>
|
||||
<ContextMenuTrigger>
|
||||
<Container onClick={handleClick} className={isActive ? 'active' : ''}>
|
||||
<AssistantNameRow className="name" title={agent.name}>
|
||||
<AgentLabel />
|
||||
</AssistantNameRow>
|
||||
</Container>
|
||||
</ContextMenuTrigger>
|
||||
<ContextMenuContent>
|
||||
<ContextMenuItem
|
||||
key="edit"
|
||||
onClick={() => {
|
||||
onOpen()
|
||||
}}>
|
||||
<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} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@ -86,32 +95,4 @@ const AssistantNameRow: React.FC<React.HTMLAttributes<HTMLDivElement>> = ({ clas
|
||||
/>
|
||||
)
|
||||
|
||||
// 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