mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2025-12-31 16:49:07 +08:00
feat(agent): implement AddAgentPopup
- Add isAgentType function to validate agent type strings - Update i18n files with new agent type validation messages - Add common validation error messages and success notifications
This commit is contained in:
parent
fa380619ce
commit
702612e3f9
@ -2,6 +2,7 @@ import {
|
||||
Avatar,
|
||||
Button,
|
||||
Form,
|
||||
Input,
|
||||
Modal,
|
||||
ModalBody,
|
||||
ModalContent,
|
||||
@ -10,26 +11,47 @@ import {
|
||||
Select,
|
||||
SelectedItemProps,
|
||||
SelectedItems,
|
||||
SelectItem
|
||||
SelectItem,
|
||||
Textarea
|
||||
} from '@heroui/react'
|
||||
import ClaudeCodeIcon from '@renderer/assets/images/models/claude.png'
|
||||
import { loggerService } from '@logger'
|
||||
import ClaudeIcon from '@renderer/assets/images/models/claude.png'
|
||||
import { TopView } from '@renderer/components/TopView'
|
||||
import { useAgents } from '@renderer/hooks/useAgents'
|
||||
import { useTimer } from '@renderer/hooks/useTimer'
|
||||
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
|
||||
import { AgentEntity } from '@renderer/types'
|
||||
import { AgentEntity, isAgentType } from '@renderer/types'
|
||||
import { uuid } from '@renderer/utils'
|
||||
import { useCallback, useMemo, useRef, useState } from 'react'
|
||||
import { ChangeEvent, FormEvent, useCallback, useMemo, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import { ErrorBoundary } from '../ErrorBoundary'
|
||||
|
||||
const logger = loggerService.withContext('AddAgentPopup')
|
||||
interface Props {
|
||||
resolve: (value: AgentEntity | undefined) => void
|
||||
}
|
||||
|
||||
type AgentTypeOption = {
|
||||
interface Option {
|
||||
key: string
|
||||
label: string
|
||||
// img src
|
||||
avatar: string
|
||||
}
|
||||
|
||||
interface AgentTypeOption extends Option {
|
||||
key: AgentEntity['type']
|
||||
name: AgentEntity['name']
|
||||
avatar: AgentEntity['avatar']
|
||||
}
|
||||
|
||||
type ModelOption = Option
|
||||
|
||||
type AgentForm = {
|
||||
type: AgentEntity['type']
|
||||
name: AgentEntity['name']
|
||||
description?: AgentEntity['description']
|
||||
instructions?: AgentEntity['instructions']
|
||||
model?: AgentEntity['model']
|
||||
}
|
||||
|
||||
const PopupContainer: React.FC<Props> = ({ resolve }) => {
|
||||
@ -37,10 +59,17 @@ const PopupContainer: React.FC<Props> = ({ resolve }) => {
|
||||
const { t } = useTranslation()
|
||||
const loadingRef = useRef(false)
|
||||
const { setTimeoutTimer } = useTimer()
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { addAgent } = useAgents()
|
||||
// default values. may change to undefined.
|
||||
const [form, setForm] = useState<AgentForm>({
|
||||
type: 'claude-code',
|
||||
name: 'Claude Code',
|
||||
model: 'claude-4-sonnet'
|
||||
})
|
||||
|
||||
const Option = useCallback(
|
||||
({ option }: { option?: AgentTypeOption | null }) => {
|
||||
({ option }: { option?: Option | null }) => {
|
||||
if (!option) {
|
||||
return (
|
||||
<div className="flex gap-2">
|
||||
@ -52,31 +81,29 @@ const PopupContainer: React.FC<Props> = ({ resolve }) => {
|
||||
return (
|
||||
<div className="flex gap-2">
|
||||
<Avatar src={option.avatar} className="h-5 w-5" />
|
||||
{option.name}
|
||||
{option.label}
|
||||
</div>
|
||||
)
|
||||
},
|
||||
[t]
|
||||
)
|
||||
|
||||
const Item = useCallback(
|
||||
({ item }: { item: SelectedItemProps<AgentTypeOption> }) => <Option option={item.data} />,
|
||||
[Option]
|
||||
)
|
||||
const Item = useCallback(({ item }: { item: SelectedItemProps<Option> }) => <Option option={item.data} />, [Option])
|
||||
|
||||
const renderValue = useCallback(
|
||||
(items: SelectedItems<AgentTypeOption>) => items.map((item) => <Item key={item.key} item={item} />),
|
||||
const renderOption = useCallback(
|
||||
(items: SelectedItems<Option>) => items.map((item) => <Item key={item.key} item={item} />),
|
||||
[Item]
|
||||
)
|
||||
|
||||
// add supported agents type here
|
||||
// add supported agents type here.
|
||||
const agentConfig = useMemo(
|
||||
() =>
|
||||
[
|
||||
{
|
||||
key: 'claude-code',
|
||||
label: 'Claude Code',
|
||||
name: 'Claude Code',
|
||||
avatar: ClaudeCodeIcon
|
||||
avatar: ClaudeIcon
|
||||
}
|
||||
] as const satisfies AgentTypeOption[],
|
||||
[]
|
||||
@ -94,74 +121,169 @@ const PopupContainer: React.FC<Props> = ({ resolve }) => {
|
||||
[Option, agentConfig]
|
||||
)
|
||||
|
||||
const onCreateAgent = useCallback(
|
||||
async (option: AgentTypeOption) => {
|
||||
const onAgentTypeChange = useCallback(
|
||||
(e: ChangeEvent<HTMLSelectElement>) => {
|
||||
const prevConfig = agentConfig.find((config) => config.key === form.type)
|
||||
let newName: string = form.name
|
||||
if (prevConfig && prevConfig.name === form.name) {
|
||||
const newConfig = agentConfig.find((config) => config.key === e.target.value)
|
||||
if (newConfig) {
|
||||
newName = newConfig.name
|
||||
}
|
||||
}
|
||||
setForm((prev) => ({
|
||||
...prev,
|
||||
type: e.target.value as AgentForm['type'],
|
||||
name: newName
|
||||
}))
|
||||
},
|
||||
[agentConfig, form.name, form.type]
|
||||
)
|
||||
|
||||
const onNameChange = useCallback((name: string) => {
|
||||
setForm((prev) => ({
|
||||
...prev,
|
||||
name
|
||||
}))
|
||||
}, [])
|
||||
|
||||
const onDescChange = useCallback((description: string) => {
|
||||
setForm((prev) => ({
|
||||
...prev,
|
||||
description
|
||||
}))
|
||||
}, [])
|
||||
|
||||
const onInstChange = useCallback((instructions: string) => {
|
||||
setForm((prev) => ({
|
||||
...prev,
|
||||
instructions
|
||||
}))
|
||||
}, [])
|
||||
|
||||
const modelOptions = useMemo(() => {
|
||||
// mocked data. not final version
|
||||
return [
|
||||
{
|
||||
key: 'claude-4-sonnet',
|
||||
label: 'Claude 4 Sonnet',
|
||||
avatar: ClaudeIcon
|
||||
}
|
||||
] satisfies ModelOption[]
|
||||
}, [])
|
||||
|
||||
const onModelChange = useCallback((e: ChangeEvent<HTMLSelectElement>) => {
|
||||
setForm((prev) => ({
|
||||
...prev,
|
||||
model: e.target.value
|
||||
}))
|
||||
}, [])
|
||||
|
||||
const onSubmit = useCallback(
|
||||
async (e: FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault()
|
||||
if (loadingRef.current) {
|
||||
return
|
||||
}
|
||||
|
||||
loadingRef.current = true
|
||||
// TODO: update redux state
|
||||
|
||||
// Additional validation check besides native HTML validation to ensure security
|
||||
if (!isAgentType(form.type)) {
|
||||
window.toast.error(t('agent.add.error.invalid_agent'))
|
||||
return
|
||||
}
|
||||
if (form.model === undefined) {
|
||||
window.toast.error(t('error.model.not_exists'))
|
||||
return
|
||||
}
|
||||
|
||||
const agent = {
|
||||
id: uuid(),
|
||||
type: option.key,
|
||||
name: option.name,
|
||||
created_at: '',
|
||||
updated_at: '',
|
||||
model: ''
|
||||
type: form.type,
|
||||
name: form.name,
|
||||
description: form.description,
|
||||
instructions: form.instructions,
|
||||
created_at: new Date().toISOString(),
|
||||
updated_at: new Date().toISOString(),
|
||||
model: form.model
|
||||
} satisfies AgentEntity
|
||||
logger.debug('Agent', agent)
|
||||
// addAgent(agent)
|
||||
window.toast.success(t('common.add_success'))
|
||||
|
||||
setTimeoutTimer('onCreateAgent', () => EventEmitter.emit(EVENT_NAMES.SHOW_ASSISTANTS), 0)
|
||||
resolve(agent)
|
||||
setOpen(false)
|
||||
},
|
||||
[setTimeoutTimer, resolve]
|
||||
[form.type, form.model, form.name, form.description, form.instructions, t, setTimeoutTimer, resolve]
|
||||
)
|
||||
|
||||
const onClose = async () => {
|
||||
setOpen(false)
|
||||
AddAgentPopup.hide()
|
||||
resolve(undefined)
|
||||
}
|
||||
|
||||
const onSubmit = async () => {
|
||||
window.toast.info('not implemented :(')
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal isOpen={open} onClose={onClose}>
|
||||
<ModalContent>
|
||||
{(onClose) => (
|
||||
<>
|
||||
<ModalHeader>{t('agent.add.title')}</ModalHeader>
|
||||
<ModalBody>
|
||||
<Form>
|
||||
<Select
|
||||
items={agentOptions}
|
||||
label={t('agent.add.label')}
|
||||
placeholder={t('agent.add.placeholder')}
|
||||
renderValue={renderValue}>
|
||||
{(option) => (
|
||||
<SelectItem key={option.key}>
|
||||
<Option option={option} />
|
||||
</SelectItem>
|
||||
)}
|
||||
</Select>
|
||||
<ErrorBoundary>
|
||||
<Modal isOpen={open} onClose={onClose}>
|
||||
<ModalContent>
|
||||
{(onClose) => (
|
||||
<>
|
||||
<ModalHeader>{t('agent.add.title')}</ModalHeader>
|
||||
<Form onSubmit={onSubmit} className="w-full">
|
||||
<ModalBody className="w-full">
|
||||
<Select
|
||||
isRequired
|
||||
selectionMode="single"
|
||||
selectedKeys={[form.type]}
|
||||
onChange={onAgentTypeChange}
|
||||
items={agentOptions}
|
||||
label={t('agent.add.type.label')}
|
||||
placeholder={t('agent.add.type.placeholder')}
|
||||
renderValue={renderOption}>
|
||||
{(option) => (
|
||||
<SelectItem key={option.key} textValue={option.label}>
|
||||
<Option option={option} />
|
||||
</SelectItem>
|
||||
)}
|
||||
</Select>
|
||||
<Input isRequired value={form.name} onValueChange={onNameChange} label={t('common.name')} />
|
||||
{/* Model type definition is string. It cannot be related to provider. Just mock a model now. */}
|
||||
<Select
|
||||
isRequired
|
||||
selectionMode="single"
|
||||
selectedKeys={form.model ? [form.model] : []}
|
||||
onChange={onModelChange}
|
||||
items={modelOptions}
|
||||
label={t('common.model')}
|
||||
placeholder={t('common.placeholders.select.model')}
|
||||
renderValue={renderOption}>
|
||||
{(option) => (
|
||||
<SelectItem key={option.key} textValue={option.label}>
|
||||
<Option option={option} />
|
||||
</SelectItem>
|
||||
)}
|
||||
</Select>
|
||||
<Textarea label={t('common.description')} value={form.description} onValueChange={onDescChange} />
|
||||
<Textarea label={t('common.prompt')} value={form.instructions} onValueChange={onInstChange} />
|
||||
</ModalBody>
|
||||
<ModalFooter className="w-full">
|
||||
<Button onPress={onClose}>{t('common.close')}</Button>
|
||||
<Button color="primary" type="submit" isLoading={loadingRef.current}>
|
||||
{t('common.add')}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</Form>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button onPress={onClose}>{t('common.close')}</Button>
|
||||
<Button color="primary" onPress={onSubmit}>
|
||||
{t('common.add')}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</>
|
||||
)}
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
</>
|
||||
)}
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
</ErrorBoundary>
|
||||
)
|
||||
}
|
||||
|
||||
// FIXME: Under the current TopView design, the close animation will fail.
|
||||
export default class AddAgentPopup {
|
||||
static topviewId = 0
|
||||
static hide() {
|
||||
|
||||
@ -1,4 +1,16 @@
|
||||
{
|
||||
"agent": {
|
||||
"add": {
|
||||
"error": {
|
||||
"invalid_agent": "Invalid Agent"
|
||||
},
|
||||
"title": "Add Agent",
|
||||
"type": {
|
||||
"label": "Agent Type",
|
||||
"placeholder": "Select an agent type"
|
||||
}
|
||||
}
|
||||
},
|
||||
"agents": {
|
||||
"add": {
|
||||
"button": "Add to Assistant",
|
||||
@ -731,6 +743,7 @@
|
||||
},
|
||||
"common": {
|
||||
"add": "Add",
|
||||
"add_success": "Added successfully",
|
||||
"advanced_settings": "Advanced Settings",
|
||||
"and": "and",
|
||||
"assistant": "Assistant",
|
||||
@ -769,6 +782,7 @@
|
||||
"go_to_settings": "Go to settings",
|
||||
"i_know": "I know",
|
||||
"inspect": "Inspect",
|
||||
"invalid_value": "Invalid Value",
|
||||
"knowledge_base": "Knowledge Base",
|
||||
"language": "Language",
|
||||
"loading": "Loading...",
|
||||
@ -780,6 +794,11 @@
|
||||
"none": "None",
|
||||
"open": "Open",
|
||||
"paste": "Paste",
|
||||
"placeholders": {
|
||||
"select": {
|
||||
"model": "Select a model"
|
||||
}
|
||||
},
|
||||
"preview": "Preview",
|
||||
"prompt": "Prompt",
|
||||
"provider": "Provider",
|
||||
|
||||
@ -1,9 +1,14 @@
|
||||
{
|
||||
"agent": {
|
||||
"add": {
|
||||
"label": "Agent 类型",
|
||||
"placeholder": "选择 Agent 类型",
|
||||
"title": "添加 Agent"
|
||||
"error": {
|
||||
"invalid_agent": "无效的 Agent"
|
||||
},
|
||||
"title": "添加 Agent",
|
||||
"type": {
|
||||
"label": "Agent 类型",
|
||||
"placeholder": "选择 Agent 类型"
|
||||
}
|
||||
}
|
||||
},
|
||||
"agents": {
|
||||
@ -738,6 +743,7 @@
|
||||
},
|
||||
"common": {
|
||||
"add": "添加",
|
||||
"add_success": "添加成功",
|
||||
"advanced_settings": "高级设置",
|
||||
"and": "和",
|
||||
"assistant": "智能体",
|
||||
@ -766,6 +772,9 @@
|
||||
"edit": "编辑",
|
||||
"enabled": "已启用",
|
||||
"error": "错误",
|
||||
"errors": {
|
||||
"validation": "验证失败"
|
||||
},
|
||||
"expand": "展开",
|
||||
"file": {
|
||||
"not_supported": "不支持的文件类型 {{type}}"
|
||||
@ -788,6 +797,11 @@
|
||||
"none": "无",
|
||||
"open": "打开",
|
||||
"paste": "粘贴",
|
||||
"placeholders": {
|
||||
"select": {
|
||||
"model": "选择模型"
|
||||
}
|
||||
},
|
||||
"preview": "预览",
|
||||
"prompt": "提示词",
|
||||
"provider": "提供商",
|
||||
|
||||
@ -8,6 +8,10 @@ export type PermissionMode = 'readOnly' | 'acceptEdits' | 'bypassPermissions'
|
||||
export type SessionMessageRole = 'user' | 'agent' | 'system' | 'tool'
|
||||
export type AgentType = 'claude-code' | 'codex' | 'qwen-cli' | 'gemini-cli' | 'custom'
|
||||
|
||||
export function isAgentType(value: string): value is AgentType {
|
||||
return ['claude-code', 'codex', 'qwen-cli', 'gemini-cli', 'custom'].includes(value)
|
||||
}
|
||||
|
||||
export type SessionMessageType =
|
||||
| 'message' // User or agent message
|
||||
| 'thought' // Agent's internal reasoning/planning
|
||||
|
||||
Loading…
Reference in New Issue
Block a user