feat(agent-settings): add prompt settings tab and refactor essential settings

- Introduce new AgentPromptSettings component for managing agent prompts
- Move prompt-related functionality from AgentEssentialSettings to new component
- Add avatar display to essential settings
- Improve layout structure and styling for both settings components
This commit is contained in:
icarus 2025-09-21 21:27:04 +08:00
parent 4216ffd0da
commit bfe2e87f59
3 changed files with 180 additions and 138 deletions

View File

@ -1,18 +1,11 @@
import CodeEditor from '@renderer/components/CodeEditor'
import { Box, HSpaceBetweenStack, HStack } from '@renderer/components/Layout'
import { RichEditorRef } from '@renderer/components/RichEditor/types'
import { Avatar } from '@heroui/react'
import { Box, HStack } from '@renderer/components/Layout'
import { getAgentAvatar } from '@renderer/config/agent'
import { useUpdateAgent } from '@renderer/hooks/agents/useUpdateAgent'
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 { Input } from 'antd'
import { FC, 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
@ -22,41 +15,22 @@ interface AgentEssentialSettingsProps {
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
const _agent = { ...agent, type: undefined, name: name.trim() } satisfies UpdateAgentForm
update(_agent)
}
const promptVarsContent = <pre>{t('agents.add.prompt.variables.tip.content')}</pre>
if (!agent) return null
return (
<Container>
<div className="flex flex-1 flex-col overflow-hidden">
<Box mb={8} style={{ fontWeight: 'bold' }}>
{t('common.name')}
</Box>
<HStack gap={8} alignItems="center">
<Avatar src={getAgentAvatar(agent.type)} title={agent.type} className="h-5 w-5" />
<Input
placeholder={t('common.assistant') + t('common.name')}
value={name}
@ -69,110 +43,8 @@ const AgentEssentialSettings: FC<AgentEssentialSettingsProps> = ({ agent, update
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>
</div>
)
}
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

View File

@ -0,0 +1,160 @@
import CodeEditor from '@renderer/components/CodeEditor'
import { Box, HSpaceBetweenStack, HStack } from '@renderer/components/Layout'
import { RichEditorRef } from '@renderer/components/RichEditor/types'
import { useUpdateAgent } from '@renderer/hooks/agents/useUpdateAgent'
import { usePromptProcessor } from '@renderer/hooks/usePromptProcessor'
import { estimateTextTokens } from '@renderer/services/TokenService'
import { AgentEntity, UpdateAgentForm } from '@renderer/types'
import { Button, 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'
interface AgentPromptSettingsProps {
agent: AgentEntity | undefined | null
update: ReturnType<typeof useUpdateAgent>
}
const AgentPromptSettings: FC<AgentPromptSettingsProps> = ({ agent, update }) => {
const { t } = useTranslation()
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, instructions } satisfies UpdateAgentForm
update(_agent)
}
const promptVarsContent = <pre>{t('agents.add.prompt.variables.tip.content')}</pre>
if (!agent) return null
return (
<Container>
<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%;
flex: 1;
`
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: 100%;
flex: 1;
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 AgentPromptSettings

View File

@ -10,6 +10,7 @@ import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
import AgentEssentialSettings from './AgentEssentialSettings'
import AgentPromptSettings from './AgentPromptSettings'
interface AgentSettingPopupShowParams {
agentId: string
@ -47,6 +48,10 @@ const AgentSettingPopupContainer: React.FC<AgentSettingPopupParams> = ({ tab, ag
{
key: 'essential',
label: t('agent.settings.essential')
},
{
key: 'prompt',
label: t('agent.settings.prompt')
}
] satisfies { key: AgentSettingPopupTab; label: string }[]
).filter(Boolean) as { key: string; label: string }[]
@ -88,7 +93,10 @@ const AgentSettingPopupContainer: React.FC<AgentSettingPopupParams> = ({ tab, ag
onSelect={({ key }) => setMenu(key as AgentSettingPopupTab)}
/>
</LeftMenu>
<Settings>{menu === 'essential' && <AgentEssentialSettings agent={agent} update={updateAgent} />}</Settings>
<Settings>
{menu === 'essential' && <AgentEssentialSettings agent={agent} update={updateAgent} />}
{menu === 'prompt' && <AgentPromptSettings agent={agent} update={updateAgent} />}
</Settings>
</HStack>
</StyledModal>
)
@ -100,6 +108,8 @@ const LeftMenu = styled.div`
`
const Settings = styled.div`
display: flex;
flex-direction: column;
flex: 1;
padding: 16px 16px;
height: calc(80vh - 16px);