mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2025-12-26 03:31:24 +08:00
refactor(agent-settings): replace agent modal with dedicated settings popup
Move agent editing functionality from inline modal to a dedicated settings popup component for better maintainability and separation of concerns. The new implementation provides a more structured settings interface with essential agent configuration options.
This commit is contained in:
parent
7999149901
commit
9b22e1671f
@ -1,7 +1,7 @@
|
||||
import { Avatar, Button, cn, useDisclosure } from '@heroui/react'
|
||||
import { Avatar, Button, cn } from '@heroui/react'
|
||||
import { DeleteIcon, EditIcon } from '@renderer/components/Icons'
|
||||
import { AgentModal } from '@renderer/components/Popups/agent/AgentModal'
|
||||
import { getAgentAvatar } from '@renderer/config/agent'
|
||||
import AgentSettingsPopup from '@renderer/pages/settings/AgentSettings'
|
||||
import { AgentEntity } from '@renderer/types'
|
||||
import { ContextMenu, ContextMenuContent, ContextMenuItem, ContextMenuTrigger } from '@renderer/ui/context-menu'
|
||||
import { FC, memo, useCallback } from 'react'
|
||||
@ -18,7 +18,7 @@ interface AgentItemProps {
|
||||
|
||||
const AgentItem: FC<AgentItemProps> = ({ agent, isActive, onDelete, onPress }) => {
|
||||
const { t } = useTranslation()
|
||||
const { isOpen, onOpen, onClose } = useDisclosure()
|
||||
// const { isOpen, onOpen, onClose } = useDisclosure()
|
||||
// const { agents } = useAgents()
|
||||
|
||||
const AgentLabel = useCallback(() => {
|
||||
@ -45,8 +45,11 @@ const AgentItem: FC<AgentItemProps> = ({ agent, isActive, onDelete, onPress }) =
|
||||
<ContextMenuContent>
|
||||
<ContextMenuItem
|
||||
key="edit"
|
||||
onClick={() => {
|
||||
onOpen()
|
||||
onClick={async () => {
|
||||
// onOpen()
|
||||
await AgentSettingsPopup.show({
|
||||
agentId: agent.id
|
||||
})
|
||||
}}>
|
||||
<EditIcon size={14} />
|
||||
{t('common.edit')}
|
||||
@ -68,7 +71,7 @@ const AgentItem: FC<AgentItemProps> = ({ agent, isActive, onDelete, onPress }) =
|
||||
</ContextMenuItem>
|
||||
</ContextMenuContent>
|
||||
</ContextMenu>
|
||||
<AgentModal isOpen={isOpen} onClose={onClose} agent={agent} />
|
||||
{/* <AgentModal isOpen={isOpen} onClose={onClose} agent={agent} /> */}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@ -0,0 +1,175 @@
|
||||
import CodeEditor from '@renderer/components/CodeEditor'
|
||||
import { Box, HSpaceBetweenStack, HStack } from '@renderer/components/Layout'
|
||||
import { RichEditorRef } from '@renderer/components/RichEditor/types'
|
||||
import { useAgent } from '@renderer/hooks/agents/useAgent'
|
||||
import { usePromptProcessor } from '@renderer/hooks/usePromptProcessor'
|
||||
import { estimateTextTokens } from '@renderer/services/TokenService'
|
||||
import { AgentEntity, UpdateAgentForm } from '@renderer/types'
|
||||
import { Button, Input, Popover } from 'antd'
|
||||
import { Edit, HelpCircle, Save } from 'lucide-react'
|
||||
import { FC, useEffect, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import ReactMarkdown from 'react-markdown'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import { SettingDivider } from '..'
|
||||
|
||||
interface AgentEssentialSettingsProps {
|
||||
agent: AgentEntity | undefined | null
|
||||
update: ReturnType<typeof useAgent>['updateAgent']
|
||||
}
|
||||
|
||||
const AgentEssentialSettings: FC<AgentEssentialSettingsProps> = ({ agent, update }) => {
|
||||
const { t } = useTranslation()
|
||||
const [name, setName] = useState<string>((agent?.name ?? '').trim())
|
||||
const [instructions, setInstructions] = useState<string>(agent?.instructions ?? '')
|
||||
const [showPreview, setShowPreview] = useState<boolean>(!!agent?.instructions?.length)
|
||||
const [tokenCount, setTokenCount] = useState(0)
|
||||
|
||||
useEffect(() => {
|
||||
const updateTokenCount = async () => {
|
||||
const count = estimateTextTokens(instructions)
|
||||
setTokenCount(count)
|
||||
}
|
||||
updateTokenCount()
|
||||
}, [instructions])
|
||||
|
||||
const editorRef = useRef<RichEditorRef>(null)
|
||||
|
||||
const processedPrompt = usePromptProcessor({
|
||||
prompt: instructions,
|
||||
modelName: agent?.model
|
||||
})
|
||||
|
||||
const onUpdate = () => {
|
||||
if (!agent) return
|
||||
const _agent = { ...agent, type: undefined, name: name.trim(), instructions } satisfies UpdateAgentForm
|
||||
update(_agent)
|
||||
window.toast.success(t('common.saved'))
|
||||
}
|
||||
|
||||
const promptVarsContent = <pre>{t('agents.add.prompt.variables.tip.content')}</pre>
|
||||
|
||||
if (!agent) return null
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<Box mb={8} style={{ fontWeight: 'bold' }}>
|
||||
{t('common.name')}
|
||||
</Box>
|
||||
<HStack gap={8} alignItems="center">
|
||||
<Input
|
||||
placeholder={t('common.assistant') + t('common.name')}
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
onBlur={onUpdate}
|
||||
style={{ flex: 1 }}
|
||||
/>
|
||||
</HStack>
|
||||
<SettingDivider />
|
||||
<HStack mb={8} alignItems="center" gap={4}>
|
||||
<Box style={{ fontWeight: 'bold' }}>{t('common.prompt')}</Box>
|
||||
<Popover title={t('agents.add.prompt.variables.tip.title')} content={promptVarsContent}>
|
||||
<HelpCircle size={14} color="var(--color-text-2)" />
|
||||
</Popover>
|
||||
</HStack>
|
||||
<TextAreaContainer>
|
||||
<RichEditorContainer>
|
||||
{showPreview ? (
|
||||
<MarkdownContainer
|
||||
onDoubleClick={() => {
|
||||
const currentScrollTop = editorRef.current?.getScrollTop?.() || 0
|
||||
setShowPreview(false)
|
||||
requestAnimationFrame(() => editorRef.current?.setScrollTop?.(currentScrollTop))
|
||||
}}>
|
||||
<ReactMarkdown>{processedPrompt || instructions}</ReactMarkdown>
|
||||
</MarkdownContainer>
|
||||
) : (
|
||||
<CodeEditor
|
||||
value={instructions}
|
||||
language="markdown"
|
||||
onChange={setInstructions}
|
||||
height="100%"
|
||||
expanded={false}
|
||||
style={{
|
||||
height: '100%'
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</RichEditorContainer>
|
||||
</TextAreaContainer>
|
||||
<HSpaceBetweenStack width="100%" justifyContent="flex-end" mt="10px">
|
||||
<TokenCount>Tokens: {tokenCount}</TokenCount>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={showPreview ? <Edit size={14} /> : <Save size={14} />}
|
||||
onClick={() => {
|
||||
const currentScrollTop = editorRef.current?.getScrollTop?.() || 0
|
||||
if (showPreview) {
|
||||
setShowPreview(false)
|
||||
requestAnimationFrame(() => editorRef.current?.setScrollTop?.(currentScrollTop))
|
||||
} else {
|
||||
onUpdate()
|
||||
requestAnimationFrame(() => {
|
||||
setShowPreview(true)
|
||||
requestAnimationFrame(() => editorRef.current?.setScrollTop?.(currentScrollTop))
|
||||
})
|
||||
}
|
||||
}}>
|
||||
{showPreview ? t('common.edit') : t('common.save')}
|
||||
</Button>
|
||||
</HSpaceBetweenStack>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
const Container = styled.div`
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
`
|
||||
|
||||
const TextAreaContainer = styled.div`
|
||||
position: relative;
|
||||
width: 100%;
|
||||
`
|
||||
|
||||
const TokenCount = styled.div`
|
||||
padding: 2px 2px;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
color: var(--color-text-2);
|
||||
user-select: none;
|
||||
`
|
||||
|
||||
const RichEditorContainer = styled.div`
|
||||
height: calc(80vh - 202px);
|
||||
border: 0.5px solid var(--color-border);
|
||||
border-radius: 5px;
|
||||
overflow: hidden;
|
||||
|
||||
.prompt-rich-editor {
|
||||
border: none;
|
||||
height: 100%;
|
||||
|
||||
.rich-editor-wrapper {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.rich-editor-content {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const MarkdownContainer = styled.div.attrs({ className: 'markdown' })`
|
||||
height: 100%;
|
||||
padding: 0.5em;
|
||||
overflow: auto;
|
||||
`
|
||||
|
||||
export default AgentEssentialSettings
|
||||
165
src/renderer/src/pages/settings/AgentSettings/index.tsx
Normal file
165
src/renderer/src/pages/settings/AgentSettings/index.tsx
Normal file
@ -0,0 +1,165 @@
|
||||
import { Avatar } from '@heroui/react'
|
||||
import { HStack } from '@renderer/components/Layout'
|
||||
import { TopView } from '@renderer/components/TopView'
|
||||
import { getAgentAvatar } from '@renderer/config/agent'
|
||||
import { useAgent } from '@renderer/hooks/agents/useAgent'
|
||||
import { Menu, Modal } from 'antd'
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import AgentEssentialSettings from './AgentEssentialSettings'
|
||||
|
||||
interface AgentSettingPopupShowParams {
|
||||
agentId: string
|
||||
tab?: AgentSettingPopupTab
|
||||
}
|
||||
|
||||
interface AgentSettingPopupParams extends AgentSettingPopupShowParams {
|
||||
resolve: () => void
|
||||
}
|
||||
|
||||
type AgentSettingPopupTab = 'essential' | 'prompt'
|
||||
|
||||
const AgentSettingPopupContainer: React.FC<AgentSettingPopupParams> = ({ tab, agentId, resolve }) => {
|
||||
const [open, setOpen] = useState(true)
|
||||
const { t } = useTranslation()
|
||||
const [menu, setMenu] = useState<AgentSettingPopupTab>(tab || 'essential')
|
||||
|
||||
const { agent, updateAgent } = useAgent(agentId)
|
||||
|
||||
const onOk = () => {
|
||||
setOpen(false)
|
||||
}
|
||||
|
||||
const onCancel = () => {
|
||||
setOpen(false)
|
||||
}
|
||||
|
||||
const afterClose = () => {
|
||||
resolve()
|
||||
}
|
||||
|
||||
const items = (
|
||||
[
|
||||
{
|
||||
key: 'essential',
|
||||
label: t('agent.settings.essential')
|
||||
}
|
||||
] satisfies { key: AgentSettingPopupTab; label: string }[]
|
||||
).filter(Boolean) as { key: string; label: string }[]
|
||||
|
||||
return (
|
||||
<StyledModal
|
||||
open={open}
|
||||
onOk={onOk}
|
||||
onCancel={onCancel}
|
||||
afterClose={afterClose}
|
||||
maskClosable={false}
|
||||
footer={null}
|
||||
title={
|
||||
<div className="flex items-center">
|
||||
<Avatar size="sm" className="mr-2 h-5 w-5" src={agent ? getAgentAvatar(agent.type) : undefined} />
|
||||
<span className="font-extrabold text-xl">{agent?.name ?? ''}</span>
|
||||
</div>
|
||||
}
|
||||
transitionName="animation-move-down"
|
||||
styles={{
|
||||
content: {
|
||||
padding: 0,
|
||||
overflow: 'hidden'
|
||||
},
|
||||
header: { padding: '10px 15px', borderBottom: '0.5px solid var(--color-border)', margin: 0, borderRadius: 0 },
|
||||
body: {
|
||||
padding: 0
|
||||
}
|
||||
}}
|
||||
width="min(800px, 70vw)"
|
||||
height="80vh"
|
||||
centered>
|
||||
<HStack>
|
||||
<LeftMenu>
|
||||
<StyledMenu
|
||||
defaultSelectedKeys={[tab || 'essential'] satisfies AgentSettingPopupTab[]}
|
||||
mode="vertical"
|
||||
items={items}
|
||||
onSelect={({ key }) => setMenu(key as AgentSettingPopupTab)}
|
||||
/>
|
||||
</LeftMenu>
|
||||
<Settings>{menu === 'essential' && <AgentEssentialSettings agent={agent} update={updateAgent} />}</Settings>
|
||||
</HStack>
|
||||
</StyledModal>
|
||||
)
|
||||
}
|
||||
|
||||
const LeftMenu = styled.div`
|
||||
height: calc(80vh - 20px);
|
||||
border-right: 0.5px solid var(--color-border);
|
||||
`
|
||||
|
||||
const Settings = styled.div`
|
||||
flex: 1;
|
||||
padding: 16px 16px;
|
||||
height: calc(80vh - 16px);
|
||||
overflow-y: scroll;
|
||||
`
|
||||
|
||||
const StyledModal = styled(Modal)`
|
||||
.ant-modal-title {
|
||||
font-size: 14px;
|
||||
}
|
||||
.ant-modal-close {
|
||||
top: 4px;
|
||||
right: 4px;
|
||||
}
|
||||
.ant-menu-item {
|
||||
height: 36px;
|
||||
color: var(--color-text-2);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border: 0.5px solid transparent;
|
||||
border-radius: 6px;
|
||||
.ant-menu-title-content {
|
||||
line-height: 36px;
|
||||
}
|
||||
}
|
||||
.ant-menu-item-active {
|
||||
background-color: var(--color-background-soft) !important;
|
||||
transition: none;
|
||||
}
|
||||
.ant-menu-item-selected {
|
||||
background-color: var(--color-background-soft);
|
||||
border: 0.5px solid var(--color-border);
|
||||
.ant-menu-title-content {
|
||||
color: var(--color-text-1);
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const StyledMenu = styled(Menu)`
|
||||
width: 220px;
|
||||
padding: 5px;
|
||||
background: transparent;
|
||||
margin-top: 2px;
|
||||
.ant-menu-item {
|
||||
margin-bottom: 7px;
|
||||
}
|
||||
`
|
||||
|
||||
export default class AgentSettingsPopup {
|
||||
static show(props: AgentSettingPopupShowParams) {
|
||||
return new Promise<void>((resolve) => {
|
||||
TopView.show(
|
||||
<AgentSettingPopupContainer
|
||||
{...props}
|
||||
resolve={() => {
|
||||
resolve()
|
||||
TopView.hide('AgentSettingsPopup')
|
||||
}}
|
||||
/>,
|
||||
'AgentSettingsPopup'
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user