mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2026-01-07 13:59:28 +08:00
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:
parent
4216ffd0da
commit
bfe2e87f59
@ -1,18 +1,11 @@
|
|||||||
import CodeEditor from '@renderer/components/CodeEditor'
|
import { Avatar } from '@heroui/react'
|
||||||
import { Box, HSpaceBetweenStack, HStack } from '@renderer/components/Layout'
|
import { Box, HStack } from '@renderer/components/Layout'
|
||||||
import { RichEditorRef } from '@renderer/components/RichEditor/types'
|
import { getAgentAvatar } from '@renderer/config/agent'
|
||||||
import { useUpdateAgent } from '@renderer/hooks/agents/useUpdateAgent'
|
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 { AgentEntity, UpdateAgentForm } from '@renderer/types'
|
||||||
import { Button, Input, Popover } from 'antd'
|
import { Input } from 'antd'
|
||||||
import { Edit, HelpCircle, Save } from 'lucide-react'
|
import { FC, useState } from 'react'
|
||||||
import { FC, useEffect, useRef, useState } from 'react'
|
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import ReactMarkdown from 'react-markdown'
|
|
||||||
import styled from 'styled-components'
|
|
||||||
|
|
||||||
import { SettingDivider } from '..'
|
|
||||||
|
|
||||||
interface AgentEssentialSettingsProps {
|
interface AgentEssentialSettingsProps {
|
||||||
agent: AgentEntity | undefined | null
|
agent: AgentEntity | undefined | null
|
||||||
@ -22,41 +15,22 @@ interface AgentEssentialSettingsProps {
|
|||||||
const AgentEssentialSettings: FC<AgentEssentialSettingsProps> = ({ agent, update }) => {
|
const AgentEssentialSettings: FC<AgentEssentialSettingsProps> = ({ agent, update }) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const [name, setName] = useState<string>((agent?.name ?? '').trim())
|
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 = () => {
|
const onUpdate = () => {
|
||||||
if (!agent) return
|
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)
|
update(_agent)
|
||||||
}
|
}
|
||||||
|
|
||||||
const promptVarsContent = <pre>{t('agents.add.prompt.variables.tip.content')}</pre>
|
|
||||||
|
|
||||||
if (!agent) return null
|
if (!agent) return null
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container>
|
<div className="flex flex-1 flex-col overflow-hidden">
|
||||||
<Box mb={8} style={{ fontWeight: 'bold' }}>
|
<Box mb={8} style={{ fontWeight: 'bold' }}>
|
||||||
{t('common.name')}
|
{t('common.name')}
|
||||||
</Box>
|
</Box>
|
||||||
<HStack gap={8} alignItems="center">
|
<HStack gap={8} alignItems="center">
|
||||||
|
<Avatar src={getAgentAvatar(agent.type)} title={agent.type} className="h-5 w-5" />
|
||||||
<Input
|
<Input
|
||||||
placeholder={t('common.assistant') + t('common.name')}
|
placeholder={t('common.assistant') + t('common.name')}
|
||||||
value={name}
|
value={name}
|
||||||
@ -69,110 +43,8 @@ const AgentEssentialSettings: FC<AgentEssentialSettingsProps> = ({ agent, update
|
|||||||
style={{ flex: 1 }}
|
style={{ flex: 1 }}
|
||||||
/>
|
/>
|
||||||
</HStack>
|
</HStack>
|
||||||
<SettingDivider />
|
</div>
|
||||||
<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
|
export default AgentEssentialSettings
|
||||||
|
|||||||
@ -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
|
||||||
@ -10,6 +10,7 @@ import { useTranslation } from 'react-i18next'
|
|||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
|
|
||||||
import AgentEssentialSettings from './AgentEssentialSettings'
|
import AgentEssentialSettings from './AgentEssentialSettings'
|
||||||
|
import AgentPromptSettings from './AgentPromptSettings'
|
||||||
|
|
||||||
interface AgentSettingPopupShowParams {
|
interface AgentSettingPopupShowParams {
|
||||||
agentId: string
|
agentId: string
|
||||||
@ -47,6 +48,10 @@ const AgentSettingPopupContainer: React.FC<AgentSettingPopupParams> = ({ tab, ag
|
|||||||
{
|
{
|
||||||
key: 'essential',
|
key: 'essential',
|
||||||
label: t('agent.settings.essential')
|
label: t('agent.settings.essential')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'prompt',
|
||||||
|
label: t('agent.settings.prompt')
|
||||||
}
|
}
|
||||||
] satisfies { key: AgentSettingPopupTab; label: string }[]
|
] satisfies { key: AgentSettingPopupTab; label: string }[]
|
||||||
).filter(Boolean) as { key: string; 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)}
|
onSelect={({ key }) => setMenu(key as AgentSettingPopupTab)}
|
||||||
/>
|
/>
|
||||||
</LeftMenu>
|
</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>
|
</HStack>
|
||||||
</StyledModal>
|
</StyledModal>
|
||||||
)
|
)
|
||||||
@ -100,6 +108,8 @@ const LeftMenu = styled.div`
|
|||||||
`
|
`
|
||||||
|
|
||||||
const Settings = styled.div`
|
const Settings = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
padding: 16px 16px;
|
padding: 16px 16px;
|
||||||
height: calc(80vh - 16px);
|
height: calc(80vh - 16px);
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user