mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2025-12-23 01:50:13 +08:00
parent
de8dbb2646
commit
8f6bf11320
103
src/renderer/src/components/VariableList.tsx
Normal file
103
src/renderer/src/components/VariableList.tsx
Normal 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="Insert 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
|
||||
@ -165,7 +165,15 @@ const visionAllowedModels = [
|
||||
'gemma-3(?:-[\\w-]+)'
|
||||
]
|
||||
|
||||
const visionExcludedModels = ['gpt-4-\\d+-preview', 'gpt-4-turbo-preview', 'gpt-4-32k', 'gpt-4-\\d+', 'o1-mini', 'o1-preview', 'AIDC-AI/Marco-o1']
|
||||
const visionExcludedModels = [
|
||||
'gpt-4-\\d+-preview',
|
||||
'gpt-4-turbo-preview',
|
||||
'gpt-4-32k',
|
||||
'gpt-4-\\d+',
|
||||
'o1-mini',
|
||||
'o1-preview',
|
||||
'AIDC-AI/Marco-o1'
|
||||
]
|
||||
export const VISION_REGEX = new RegExp(
|
||||
`\\b(?!(?:${visionExcludedModels.join('|')})\\b)(${visionAllowedModels.join('|')})\\b`,
|
||||
'i'
|
||||
@ -203,7 +211,13 @@ export const FUNCTION_CALLING_MODELS = [
|
||||
'gemini(?:-[\\w-]+)?' // 提前排除了gemini的嵌入模型
|
||||
]
|
||||
|
||||
const FUNCTION_CALLING_EXCLUDED_MODELS = ['aqa(?:-[\\w-]+)?', 'imagen(?:-[\\w-]+)?', 'o1-mini', 'o1-preview', 'AIDC-AI/Marco-o1']
|
||||
const FUNCTION_CALLING_EXCLUDED_MODELS = [
|
||||
'aqa(?:-[\\w-]+)?',
|
||||
'imagen(?:-[\\w-]+)?',
|
||||
'o1-mini',
|
||||
'o1-preview',
|
||||
'AIDC-AI/Marco-o1'
|
||||
]
|
||||
|
||||
export const FUNCTION_CALLING_REGEX = new RegExp(
|
||||
`\\b(?!(?:${FUNCTION_CALLING_EXCLUDED_MODELS.join('|')})\\b)(?:${FUNCTION_CALLING_MODELS.join('|')})\\b`,
|
||||
|
||||
@ -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'),
|
||||
|
||||
@ -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')}
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -17,6 +17,7 @@ export type Assistant = {
|
||||
messages?: AssistantMessage[]
|
||||
enableWebSearch?: boolean
|
||||
enableGenerateImage?: boolean
|
||||
promptVariables?: Variable[]
|
||||
}
|
||||
|
||||
export type AssistantMessage = {
|
||||
@ -43,6 +44,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 = {
|
||||
|
||||
@ -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 }
|
||||
|
||||
Loading…
Reference in New Issue
Block a user