From 9b22e1671fc40e375cb5ff4ed06adf9c5b554962 Mon Sep 17 00:00:00 2001 From: icarus Date: Sat, 20 Sep 2025 21:11:58 +0800 Subject: [PATCH] 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. --- .../pages/home/Tabs/components/AgentItem.tsx | 15 +- .../AgentSettings/AgentEssentialSettings.tsx | 175 ++++++++++++++++++ .../pages/settings/AgentSettings/index.tsx | 165 +++++++++++++++++ 3 files changed, 349 insertions(+), 6 deletions(-) create mode 100644 src/renderer/src/pages/settings/AgentSettings/AgentEssentialSettings.tsx create mode 100644 src/renderer/src/pages/settings/AgentSettings/index.tsx diff --git a/src/renderer/src/pages/home/Tabs/components/AgentItem.tsx b/src/renderer/src/pages/home/Tabs/components/AgentItem.tsx index 229575ef15..18f7ab98eb 100644 --- a/src/renderer/src/pages/home/Tabs/components/AgentItem.tsx +++ b/src/renderer/src/pages/home/Tabs/components/AgentItem.tsx @@ -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 = ({ 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 = ({ agent, isActive, onDelete, onPress }) = { - onOpen() + onClick={async () => { + // onOpen() + await AgentSettingsPopup.show({ + agentId: agent.id + }) }}> {t('common.edit')} @@ -68,7 +71,7 @@ const AgentItem: FC = ({ agent, isActive, onDelete, onPress }) = - + {/* */} ) } diff --git a/src/renderer/src/pages/settings/AgentSettings/AgentEssentialSettings.tsx b/src/renderer/src/pages/settings/AgentSettings/AgentEssentialSettings.tsx new file mode 100644 index 0000000000..97a125543d --- /dev/null +++ b/src/renderer/src/pages/settings/AgentSettings/AgentEssentialSettings.tsx @@ -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['updateAgent'] +} + +const AgentEssentialSettings: FC = ({ agent, update }) => { + const { t } = useTranslation() + const [name, setName] = useState((agent?.name ?? '').trim()) + const [instructions, setInstructions] = useState(agent?.instructions ?? '') + const [showPreview, setShowPreview] = useState(!!agent?.instructions?.length) + const [tokenCount, setTokenCount] = useState(0) + + useEffect(() => { + const updateTokenCount = async () => { + const count = estimateTextTokens(instructions) + setTokenCount(count) + } + updateTokenCount() + }, [instructions]) + + const editorRef = useRef(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 =
{t('agents.add.prompt.variables.tip.content')}
+ + if (!agent) return null + + return ( + + + {t('common.name')} + + + setName(e.target.value)} + onBlur={onUpdate} + style={{ flex: 1 }} + /> + + + + {t('common.prompt')} + + + + + + + {showPreview ? ( + { + const currentScrollTop = editorRef.current?.getScrollTop?.() || 0 + setShowPreview(false) + requestAnimationFrame(() => editorRef.current?.setScrollTop?.(currentScrollTop)) + }}> + {processedPrompt || instructions} + + ) : ( + + )} + + + + Tokens: {tokenCount} + + + + ) +} + +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 diff --git a/src/renderer/src/pages/settings/AgentSettings/index.tsx b/src/renderer/src/pages/settings/AgentSettings/index.tsx new file mode 100644 index 0000000000..f5ba93f735 --- /dev/null +++ b/src/renderer/src/pages/settings/AgentSettings/index.tsx @@ -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 = ({ tab, agentId, resolve }) => { + const [open, setOpen] = useState(true) + const { t } = useTranslation() + const [menu, setMenu] = useState(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 ( + + + {agent?.name ?? ''} + + } + 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> + + + setMenu(key as AgentSettingPopupTab)} + /> + + {menu === 'essential' && } + + + ) +} + +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((resolve) => { + TopView.show( + { + resolve() + TopView.hide('AgentSettingsPopup') + }} + />, + 'AgentSettingsPopup' + ) + }) + } +}