Compare commits

...

5 Commits

Author SHA1 Message Date
Neal_Tan
370cfd6e9f Merge pull request #4331 from CherryHQ/main
Merge main code
2025-04-02 22:06:47 +01:00
Neal_Tan
d213bc1024 Merge branch 'main' into feat/variable_replace_prompt 2025-04-01 18:57:46 +01:00
Neal_Tan
1187a47698 Merge pull request #4129 from TeacherTan/main
feat(Assistant): Variables replace prompts
2025-03-30 02:15:18 +01:00
Neal_Tan
83d0eb07aa fix(i18n): update locales json file
关联提交 8f6bf113
2025-03-30 02:10:50 +01:00
Neal_Tan
8f6bf11320 feat(Assistant): 增加提示词变量输入
- 在编辑助手处添加了变量
- 保存智能体时可以保存变量
Fixed #4049
2025-03-30 00:51:48 +00:00
11 changed files with 285 additions and 12 deletions

View File

@@ -0,0 +1,103 @@
import { DeleteOutlined, ImportOutlined } from '@ant-design/icons'
import { VStack } from '@renderer/components/Layout'
import { Variable } from '@renderer/types'
import { Button, Input, Tooltip } from 'antd'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
interface VariableListProps {
variables: Variable[]
setVariables: (variables: Variable[]) => void
onUpdate?: (variables: Variable[]) => void
onInsertVariable?: (name: string) => void
}
const VariableList: React.FC<VariableListProps> = ({ variables, setVariables, onUpdate, onInsertVariable }) => {
const { t } = useTranslation()
const deleteVariable = (id: string) => {
const updatedVariables = variables.filter((v) => v.id !== id)
setVariables(updatedVariables)
if (onUpdate) {
onUpdate(updatedVariables)
}
}
const updateVariable = (id: string, field: 'name' | 'value', value: string) => {
// Only update the local state when typing, don't call the parent's onUpdate
const updatedVariables = variables.map((v) => (v.id === id ? { ...v, [field]: value } : v))
setVariables(updatedVariables)
}
// This function will be called when input loses focus
const handleInputBlur = () => {
if (onUpdate) {
onUpdate(variables)
}
}
return (
<VariablesContainer>
{variables.length === 0 ? (
<EmptyText>{t('common.no_variables_added')}</EmptyText>
) : (
<VStack gap={8} width="100%">
{variables.map((variable) => (
<VariableItem key={variable.id}>
<Input
placeholder={t('common.variable_name')}
value={variable.name}
onChange={(e) => updateVariable(variable.id, 'name', e.target.value)}
onBlur={handleInputBlur}
style={{ width: '30%' }}
/>
<Input
placeholder={t('common.value')}
value={variable.value}
onChange={(e) => updateVariable(variable.id, 'value', e.target.value)}
onBlur={handleInputBlur}
style={{ flex: 1 }}
/>
{onInsertVariable && (
<Tooltip title={t('common.insert_variable_into_prompt')}>
<Button type="text" onClick={() => onInsertVariable(variable.name)}>
<ImportOutlined />
</Button>
</Tooltip>
)}
<Button type="text" danger icon={<DeleteOutlined />} onClick={() => deleteVariable(variable.id)} />
</VariableItem>
))}
</VStack>
)}
</VariablesContainer>
)
}
const VariablesContainer = styled.div`
display: flex;
flex-direction: column;
align-items: center;
width: 100%;
overflow-y: auto;
max-height: 200px;
border: 1px solid var(--color-border);
border-radius: 6px;
padding: 12px;
`
const VariableItem = styled.div`
display: flex;
align-items: center;
gap: 8px;
width: 100%;
`
const EmptyText = styled.div`
color: var(--color-text-2);
opacity: 0.6;
font-style: italic;
`
export default VariableList

View File

@@ -286,7 +286,13 @@
"select": "Select",
"topics": "Topics",
"warning": "Warning",
"you": "You"
"you": "You",
"variable_name": "Variable Name",
"value": "Value",
"no_variables_added": "No variables added",
"insert_variable_into_prompt": "Insert variable into prompt",
"variables": "Variables",
"variables_help": "Add variables that need to be replaced in the text, triggered by {{variable_name}} in the replacement document"
},
"docs": {
"title": "Docs"

View File

@@ -286,7 +286,13 @@
"select": "選択",
"topics": "トピック",
"warning": "警告",
"you": "あなた"
"you": "あなた",
"variable_name": "変数名",
"value": "値",
"no_variables_added": "変数がありません",
"insert_variable_into_prompt": "プロンプトに変数を挿入",
"variables": "変数",
"variables_help": "テキスト内で置換が必要な変数を追加し、置換ドキュメント内で{{variable_name}}の形式でトリガーします"
},
"docs": {
"title": "ドキュメント"

View File

@@ -286,7 +286,13 @@
"select": "Выбрать",
"topics": "Топики",
"warning": "Предупреждение",
"you": "Вы"
"you": "Вы",
"variable_name": "Имя переменной",
"value": "Значение",
"no_variables_added": "Нет переменных",
"insert_variable_into_prompt": "Вставить переменную в промпт",
"variables": "Переменные",
"variables_help": "Добавьте переменные, которые нужно заменить в тексте, замена срабатывает в формате {{variable_name}} в документе замены"
},
"docs": {
"title": "Документация"

View File

@@ -286,7 +286,13 @@
"select": "选择",
"topics": "话题",
"warning": "警告",
"you": "用户"
"you": "用户",
"variable_name": "变量名称",
"value": "值",
"no_variables_added": "没有添加变量",
"insert_variable_into_prompt": "插入变量到提示词",
"variables": "变量",
"variables_help": "添加需要替换的变量名字和值即可"
},
"docs": {
"title": "帮助文档"

View File

@@ -286,7 +286,13 @@
"select": "選擇",
"topics": "話題",
"warning": "警告",
"you": "您"
"you": "您",
"variable_name": "變量名稱",
"value": "值",
"no_variables_added": "沒有添加變量",
"insert_variable_into_prompt": "插入變量到提示詞",
"variables": "變量",
"variables_help": "添加需要替換的變量名字和值即可"
},
"docs": {
"title": "說明文件"

View File

@@ -84,6 +84,9 @@ const AssistantItem: FC<AssistantItemProps> = ({ assistant, isActive, onSwitch,
const agent = omit(assistant, ['model', 'emoji'])
agent.id = uuid()
agent.type = 'agent'
if (assistant.promptVariables) {
agent.promptVariables = [...assistant.promptVariables]
}
addAgent(agent)
window.message.success({
content: t('assistants.save.success'),

View File

@@ -1,16 +1,18 @@
import 'emoji-picker-element'
import { CloseCircleFilled } from '@ant-design/icons'
import { CloseCircleFilled, PlusOutlined } from '@ant-design/icons'
import EmojiPicker from '@renderer/components/EmojiPicker'
import { Box, HStack } from '@renderer/components/Layout'
import VariableList from '@renderer/components/VariableList'
import { estimateTextTokens } from '@renderer/services/TokenService'
import { Assistant, AssistantSettings } from '@renderer/types'
import { Assistant, AssistantSettings, Variable } from '@renderer/types'
import { getLeadingEmoji } from '@renderer/utils'
import { Button, Input, Popover } from 'antd'
import { Button, Input, Popover, Tooltip, Typography } from 'antd'
import TextArea from 'antd/es/input/TextArea'
import { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
import { v4 as uuidv4 } from 'uuid'
interface Props {
assistant: Assistant
@@ -24,6 +26,9 @@ const AssistantPromptSettings: React.FC<Props> = ({ assistant, updateAssistant,
const [name, setName] = useState(assistant.name.replace(getLeadingEmoji(assistant.name) || '', '').trim())
const [prompt, setPrompt] = useState(assistant.prompt)
const [tokenCount, setTokenCount] = useState(0)
const [variables, setVariables] = useState<Variable[]>(assistant.promptVariables || [])
const [variableName, setVariableName] = useState('')
const [variableValue, setVariableValue] = useState('')
const { t } = useTranslation()
useEffect(() => {
@@ -35,19 +40,77 @@ const AssistantPromptSettings: React.FC<Props> = ({ assistant, updateAssistant,
}, [prompt])
const onUpdate = () => {
const _assistant = { ...assistant, name: name.trim(), emoji, prompt }
const _assistant = {
...assistant,
name: name.trim(),
emoji,
prompt,
promptVariables: variables
}
updateAssistant(_assistant)
}
const handleEmojiSelect = (selectedEmoji: string) => {
setEmoji(selectedEmoji)
const _assistant = { ...assistant, name: name.trim(), emoji: selectedEmoji, prompt }
const _assistant = {
...assistant,
name: name.trim(),
emoji: selectedEmoji,
prompt,
promptVariables: variables
}
updateAssistant(_assistant)
}
const handleEmojiDelete = () => {
setEmoji('')
const _assistant = { ...assistant, name: name.trim(), prompt, emoji: '' }
const _assistant = {
...assistant,
name: name.trim(),
prompt,
emoji: '',
promptVariables: variables
}
updateAssistant(_assistant)
}
const handleUpdateVariables = (updatedVariables: Variable[]) => {
const _assistant = {
...assistant,
name: name.trim(),
emoji,
prompt,
promptVariables: updatedVariables
}
updateAssistant(_assistant)
}
const handleInsertVariable = (varName: string) => {
const insertText = `{{${varName}}}`
setPrompt((prev) => prev + insertText)
}
const addVariable = () => {
if (!variableName.trim()) return
const newVar: Variable = {
id: uuidv4(),
name: variableName.trim(),
value: variableValue.trim()
}
const updatedVariables = [...variables, newVar]
setVariables(updatedVariables)
setVariableName('')
setVariableValue('')
const _assistant = {
...assistant,
name: name.trim(),
emoji,
prompt,
promptVariables: updatedVariables
}
updateAssistant(_assistant)
}
@@ -99,10 +162,49 @@ const AssistantPromptSettings: React.FC<Props> = ({ assistant, updateAssistant,
onChange={(e) => setPrompt(e.target.value)}
onBlur={onUpdate}
spellCheck={false}
style={{ minHeight: 'calc(80vh - 200px)', maxHeight: 'calc(80vh - 150px)' }}
style={{ minHeight: 'calc(80vh - 320px)', maxHeight: 'calc(80vh - 270px)' }}
/>
<TokenCount>Tokens: {tokenCount}</TokenCount>
</TextAreaContainer>
<Box mt={12} mb={8}>
<HStack justifyContent="space-between" alignItems="center">
<Typography.Title level={5} style={{ margin: 0 }}>
{t('common.variables')}
</Typography.Title>
<Tooltip title={t('common.variables_help')}>
<Typography.Text type="secondary" style={{ fontSize: '12px', cursor: 'help' }}>
?
</Typography.Text>
</Tooltip>
</HStack>
</Box>
<VariableList
variables={variables}
setVariables={setVariables}
onUpdate={handleUpdateVariables}
onInsertVariable={handleInsertVariable}
/>
<HStack gap={8} width="100%" mt={8} mb={8}>
<Input
placeholder={t('common.variable_name')}
value={variableName}
onChange={(e) => setVariableName(e.target.value)}
style={{ width: '30%' }}
/>
<Input
placeholder={t('common.value')}
value={variableValue}
onChange={(e) => setVariableValue(e.target.value)}
style={{ flex: 1 }}
/>
<Button type="primary" icon={<PlusOutlined />} onClick={addVariable}>
{t('common.add')}
</Button>
</HStack>
<HStack width="100%" justifyContent="flex-end" mt="10px">
<Button type="primary" onClick={onOk}>
{t('common.close')}

View File

@@ -4,6 +4,7 @@ import i18n from '@renderer/i18n'
import store from '@renderer/store'
import { setGenerating } from '@renderer/store/runtime'
import { Assistant, MCPTool, Message, Model, Provider, Suggestion } from '@renderer/types'
import { processPromptVariables } from '@renderer/utils'
import { formatMessageError, isAbortError } from '@renderer/utils/error'
import { withGenerateImage } from '@renderer/utils/formats'
import { cloneDeep, findLast, isEmpty } from 'lodash'
@@ -42,6 +43,14 @@ export async function fetchChatCompletion({
let isFirstChunk = true
let query = ''
// Process variables in the prompt if they exist
if (assistant.promptVariables && assistant.promptVariables.length > 0) {
assistant = {
...assistant,
prompt: processPromptVariables(assistant.prompt, assistant.promptVariables)
}
}
// Search web
if (WebSearchService.isWebSearchEnabled() && assistant.enableWebSearch && assistant.model) {
const webSearchParams = getOpenAIWebSearchParams(assistant, assistant.model)

View File

@@ -17,6 +17,7 @@ export type Assistant = {
messages?: AssistantMessage[]
enableWebSearch?: boolean
enableGenerateImage?: boolean
promptVariables?: Variable[]
mcpServers?: MCPServer[]
}
@@ -44,6 +45,12 @@ export type AssistantSettings = {
reasoning_effort?: 'low' | 'medium' | 'high'
}
export type Variable = {
id: string
name: string
value: string
}
export type Agent = Omit<Assistant, 'model'>
export type Message = {

View File

@@ -500,4 +500,23 @@ export function hasObjectKey(obj: any, key: string) {
return Object.keys(obj).includes(key)
}
/**
* Process variables in a prompt string
* @param prompt The prompt string containing variables in {{var_name}} format
* @param variables Array of variables with name and value
* @returns The prompt with variables replaced
*/
export function processPromptVariables(prompt: string, variables: Array<{ name: string; value: string }> = []) {
if (!prompt || !variables || variables.length === 0) {
return prompt
}
let processedPrompt = prompt
variables.forEach((variable) => {
const pattern = new RegExp(`{{${variable.name}}}`, 'g')
processedPrompt = processedPrompt.replace(pattern, variable.value)
})
return processedPrompt
}
export { classNames }