mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2026-01-07 05:39:05 +08:00
feat(agent): enhance agent settings with accessible paths management
- Add UI for managing accessible paths in agent settings - Improve error handling and loading states in agent components - Update type definitions for better type safety - Remove outdated comments and fix styling issues
This commit is contained in:
parent
82c08128b6
commit
4484f39525
@ -166,7 +166,8 @@ const api = {
|
|||||||
openPath: (path: string) => ipcRenderer.invoke(IpcChannel.File_OpenPath, path),
|
openPath: (path: string) => ipcRenderer.invoke(IpcChannel.File_OpenPath, path),
|
||||||
save: (path: string, content: string | NodeJS.ArrayBufferView, options?: any) =>
|
save: (path: string, content: string | NodeJS.ArrayBufferView, options?: any) =>
|
||||||
ipcRenderer.invoke(IpcChannel.File_Save, path, content, options),
|
ipcRenderer.invoke(IpcChannel.File_Save, path, content, options),
|
||||||
selectFolder: (options?: OpenDialogOptions) => ipcRenderer.invoke(IpcChannel.File_SelectFolder, options),
|
selectFolder: (options?: OpenDialogOptions): Promise<string | null> =>
|
||||||
|
ipcRenderer.invoke(IpcChannel.File_SelectFolder, options),
|
||||||
saveImage: (name: string, data: string) => ipcRenderer.invoke(IpcChannel.File_SaveImage, name, data),
|
saveImage: (name: string, data: string) => ipcRenderer.invoke(IpcChannel.File_SaveImage, name, data),
|
||||||
binaryImage: (fileId: string) => ipcRenderer.invoke(IpcChannel.File_BinaryImage, fileId),
|
binaryImage: (fileId: string) => ipcRenderer.invoke(IpcChannel.File_BinaryImage, fileId),
|
||||||
base64Image: (fileId: string): Promise<{ mime: string; base64: string; data: string }> =>
|
base64Image: (fileId: string): Promise<{ mime: string; base64: string; data: string }> =>
|
||||||
|
|||||||
@ -327,7 +327,6 @@ export const AgentModal: React.FC<Props> = ({ agent, trigger, isOpen: _isOpen, o
|
|||||||
)}
|
)}
|
||||||
</Select>
|
</Select>
|
||||||
<Input isRequired value={form.name} onValueChange={onNameChange} label={t('common.name')} />
|
<Input isRequired value={form.name} onValueChange={onNameChange} label={t('common.name')} />
|
||||||
{/* FIXME: Model type definition is string. It cannot be related to provider. Just mock a model now. */}
|
|
||||||
<Select
|
<Select
|
||||||
isRequired
|
isRequired
|
||||||
selectionMode="single"
|
selectionMode="single"
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { Button, Spinner } from '@heroui/react'
|
import { Alert, Button, Spinner } from '@heroui/react'
|
||||||
import { AgentModal } from '@renderer/components/Popups/agent/AgentModal'
|
import { AgentModal } from '@renderer/components/Popups/agent/AgentModal'
|
||||||
import { useAgents } from '@renderer/hooks/agents/useAgents'
|
import { useAgents } from '@renderer/hooks/agents/useAgents'
|
||||||
import { useRuntime } from '@renderer/hooks/useRuntime'
|
import { useRuntime } from '@renderer/hooks/useRuntime'
|
||||||
@ -14,7 +14,7 @@ import { SectionName } from './SectionName'
|
|||||||
interface AssistantsTabProps {}
|
interface AssistantsTabProps {}
|
||||||
|
|
||||||
export const Agents: FC<AssistantsTabProps> = () => {
|
export const Agents: FC<AssistantsTabProps> = () => {
|
||||||
const { agents, deleteAgent, isLoading } = useAgents()
|
const { agents, deleteAgent, isLoading, error } = useAgents()
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { chat } = useRuntime()
|
const { chat } = useRuntime()
|
||||||
const { activeAgentId } = chat
|
const { activeAgentId } = chat
|
||||||
@ -38,7 +38,9 @@ export const Agents: FC<AssistantsTabProps> = () => {
|
|||||||
<div className="agents-tab h-full w-full">
|
<div className="agents-tab h-full w-full">
|
||||||
<SectionName name={t('common.agent_other')} />
|
<SectionName name={t('common.agent_other')} />
|
||||||
{isLoading && <Spinner />}
|
{isLoading && <Spinner />}
|
||||||
|
{error && <Alert color="danger" title={t('agent.list.error.failed')} />}
|
||||||
{!isLoading &&
|
{!isLoading &&
|
||||||
|
!error &&
|
||||||
agents.map((agent) => (
|
agents.map((agent) => (
|
||||||
<AgentItem
|
<AgentItem
|
||||||
key={agent.id}
|
key={agent.id}
|
||||||
|
|||||||
@ -1,14 +1,19 @@
|
|||||||
|
import { Button, Tooltip } from '@heroui/react'
|
||||||
|
import { loggerService } from '@logger'
|
||||||
import { ApiModelLabel } from '@renderer/components/ApiModelLabel'
|
import { ApiModelLabel } from '@renderer/components/ApiModelLabel'
|
||||||
import { useApiModels } from '@renderer/hooks/agents/useModels'
|
import { useApiModels } from '@renderer/hooks/agents/useModels'
|
||||||
import { useUpdateAgent } from '@renderer/hooks/agents/useUpdateAgent'
|
import { useUpdateAgent } from '@renderer/hooks/agents/useUpdateAgent'
|
||||||
import { AgentEntity, UpdateAgentForm } from '@renderer/types'
|
import { AgentEntity, UpdateAgentForm } from '@renderer/types'
|
||||||
import { Input, Select } from 'antd'
|
import { Input, Select } from 'antd'
|
||||||
import { DefaultOptionType } from 'antd/es/select'
|
import { DefaultOptionType } from 'antd/es/select'
|
||||||
import { FC, useMemo, useState } from 'react'
|
import { Plus } from 'lucide-react'
|
||||||
|
import { FC, useCallback, useMemo, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
import { AgentLabel, SettingsContainer, SettingsItem, SettingsTitle } from './shared'
|
import { AgentLabel, SettingsContainer, SettingsItem, SettingsTitle } from './shared'
|
||||||
|
|
||||||
|
const logger = loggerService.withContext('AgentEssentialSettings')
|
||||||
|
|
||||||
interface AgentEssentialSettingsProps {
|
interface AgentEssentialSettingsProps {
|
||||||
agent: AgentEntity | undefined | null
|
agent: AgentEntity | undefined | null
|
||||||
update: ReturnType<typeof useUpdateAgent>
|
update: ReturnType<typeof useUpdateAgent>
|
||||||
@ -19,14 +24,25 @@ const AgentEssentialSettings: FC<AgentEssentialSettingsProps> = ({ agent, update
|
|||||||
const [name, setName] = useState<string>((agent?.name ?? '').trim())
|
const [name, setName] = useState<string>((agent?.name ?? '').trim())
|
||||||
const { models } = useApiModels({ providerType: 'anthropic' })
|
const { models } = useApiModels({ providerType: 'anthropic' })
|
||||||
const agentModel = models.find((model) => model.id === agent?.model)
|
const agentModel = models.find((model) => model.id === agent?.model)
|
||||||
const [model, setModel] = useState<string | undefined>(agentModel?.id)
|
|
||||||
|
|
||||||
const onUpdate = () => {
|
const updateName = (name: string) => {
|
||||||
if (!agent) return
|
if (!agent) return
|
||||||
const _agent = { ...agent, type: undefined, name: name.trim(), model } satisfies UpdateAgentForm
|
update({ id: agent.id, name: name.trim() })
|
||||||
update(_agent)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const updateModel = (model: UpdateAgentForm['model']) => {
|
||||||
|
if (!agent) return
|
||||||
|
update({ id: agent.id, model })
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateAccessiblePaths = useCallback(
|
||||||
|
(accessible_paths: UpdateAgentForm['accessible_paths']) => {
|
||||||
|
if (!agent) return
|
||||||
|
update({ id: agent.id, accessible_paths })
|
||||||
|
},
|
||||||
|
[agent, update]
|
||||||
|
)
|
||||||
|
|
||||||
const modelOptions = useMemo(() => {
|
const modelOptions = useMemo(() => {
|
||||||
return models.map((model) => ({
|
return models.map((model) => ({
|
||||||
value: model.id,
|
value: model.id,
|
||||||
@ -34,6 +50,40 @@ const AgentEssentialSettings: FC<AgentEssentialSettingsProps> = ({ agent, update
|
|||||||
})) satisfies DefaultOptionType[]
|
})) satisfies DefaultOptionType[]
|
||||||
}, [models])
|
}, [models])
|
||||||
|
|
||||||
|
const addAccessiblePath = useCallback(async () => {
|
||||||
|
if (!agent) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
const selected = await window.api.file.selectFolder()
|
||||||
|
if (!selected) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (agent.accessible_paths.includes(selected)) {
|
||||||
|
window.toast.warning(t('agent.session.accessible_paths.duplicate'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
updateAccessiblePaths([...agent.accessible_paths, selected])
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Failed to select accessible path:', error as Error)
|
||||||
|
window.toast.error(t('agent.session.accessible_paths.select_failed'))
|
||||||
|
}
|
||||||
|
}, [agent, t, updateAccessiblePaths])
|
||||||
|
|
||||||
|
const removeAccessiblePath = useCallback(
|
||||||
|
(path: string) => {
|
||||||
|
if (!agent) return
|
||||||
|
const newPaths = agent.accessible_paths.filter((p) => p !== path)
|
||||||
|
if (newPaths.length === 0) {
|
||||||
|
window.toast.error(t('agent.session.accessible_paths.error.at_least_one'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
updateAccessiblePaths(newPaths)
|
||||||
|
},
|
||||||
|
[agent, t, updateAccessiblePaths]
|
||||||
|
)
|
||||||
|
|
||||||
if (!agent) return null
|
if (!agent) return null
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -50,7 +100,7 @@ const AgentEssentialSettings: FC<AgentEssentialSettingsProps> = ({ agent, update
|
|||||||
onChange={(e) => setName(e.target.value)}
|
onChange={(e) => setName(e.target.value)}
|
||||||
onBlur={() => {
|
onBlur={() => {
|
||||||
if (name !== agent.name) {
|
if (name !== agent.name) {
|
||||||
onUpdate()
|
updateName(name)
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
className="max-w-80 flex-1"
|
className="max-w-80 flex-1"
|
||||||
@ -60,16 +110,39 @@ const AgentEssentialSettings: FC<AgentEssentialSettingsProps> = ({ agent, update
|
|||||||
<SettingsTitle>{t('common.model')}</SettingsTitle>
|
<SettingsTitle>{t('common.model')}</SettingsTitle>
|
||||||
<Select
|
<Select
|
||||||
options={modelOptions}
|
options={modelOptions}
|
||||||
value={model}
|
value={agent.model}
|
||||||
onChange={(value) => {
|
onChange={(value) => {
|
||||||
setModel(value)
|
updateModel(value)
|
||||||
onUpdate()
|
|
||||||
}}
|
}}
|
||||||
className="max-w-80 flex-1"
|
className="max-w-80 flex-1"
|
||||||
placeholder={t('common.placeholders.select.model')}
|
placeholder={t('common.placeholders.select.model')}
|
||||||
/>
|
/>
|
||||||
</SettingsItem>
|
</SettingsItem>
|
||||||
{/* TODO: Add accessible_paths and description */}
|
{/* TODO: Add accessible_paths and description */}
|
||||||
|
<SettingsItem>
|
||||||
|
<SettingsTitle
|
||||||
|
actions={
|
||||||
|
<Tooltip content={t('agent.session.accessible_paths.add')}>
|
||||||
|
<Button size="sm" startContent={<Plus />} isIconOnly onPress={addAccessiblePath} />
|
||||||
|
</Tooltip>
|
||||||
|
}>
|
||||||
|
{t('agent.session.accessible_paths.label')}
|
||||||
|
</SettingsTitle>
|
||||||
|
<ul className="mt-2 flex flex-col gap-2 rounded-xl border p-2">
|
||||||
|
{agent.accessible_paths.map((path) => (
|
||||||
|
<li
|
||||||
|
key={path}
|
||||||
|
className="flex items-center justify-between gap-2 rounded-medium border border-default-200 px-3 py-2">
|
||||||
|
<span className="truncate text-sm" title={path}>
|
||||||
|
{path}
|
||||||
|
</span>
|
||||||
|
<Button size="sm" variant="light" color="danger" onPress={() => removeAccessiblePath(path)}>
|
||||||
|
{t('common.delete')}
|
||||||
|
</Button>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</SettingsItem>
|
||||||
</SettingsContainer>
|
</SettingsContainer>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
import { Spinner } from '@heroui/react'
|
import { Alert, Spinner } from '@heroui/react'
|
||||||
import { HStack } from '@renderer/components/Layout'
|
|
||||||
import { TopView } from '@renderer/components/TopView'
|
import { TopView } from '@renderer/components/TopView'
|
||||||
import { useAgent } from '@renderer/hooks/agents/useAgent'
|
import { useAgent } from '@renderer/hooks/agents/useAgent'
|
||||||
import { useUpdateAgent } from '@renderer/hooks/agents/useUpdateAgent'
|
import { useUpdateAgent } from '@renderer/hooks/agents/useUpdateAgent'
|
||||||
@ -28,7 +27,7 @@ const AgentSettingPopupContainer: React.FC<AgentSettingPopupParams> = ({ tab, ag
|
|||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const [menu, setMenu] = useState<AgentSettingPopupTab>(tab || 'essential')
|
const [menu, setMenu] = useState<AgentSettingPopupTab>(tab || 'essential')
|
||||||
|
|
||||||
const { agent } = useAgent(agentId)
|
const { agent, isLoading, error } = useAgent(agentId)
|
||||||
const updateAgent = useUpdateAgent()
|
const updateAgent = useUpdateAgent()
|
||||||
|
|
||||||
const onOk = () => {
|
const onOk = () => {
|
||||||
@ -57,15 +56,24 @@ const AgentSettingPopupContainer: React.FC<AgentSettingPopupParams> = ({ tab, ag
|
|||||||
).filter(Boolean)
|
).filter(Boolean)
|
||||||
|
|
||||||
const ModalContent = () => {
|
const ModalContent = () => {
|
||||||
if (!agent) {
|
if (isLoading) {
|
||||||
|
// TODO: use skeleton for better ux
|
||||||
return <Spinner />
|
return <Spinner />
|
||||||
}
|
}
|
||||||
|
if (error) {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Alert color="danger" title={t('agent.get.error.failed')} />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<HStack>
|
<div className="flex w-full flex-1">
|
||||||
<LeftMenu>
|
<LeftMenu>
|
||||||
<StyledMenu
|
<StyledMenu
|
||||||
defaultSelectedKeys={[tab || 'essential'] satisfies AgentSettingPopupTab[]}
|
defaultSelectedKeys={[tab || 'essential'] satisfies AgentSettingPopupTab[]}
|
||||||
mode="vertical"
|
mode="vertical"
|
||||||
|
selectedKeys={[menu]}
|
||||||
items={items}
|
items={items}
|
||||||
onSelect={({ key }) => setMenu(key as AgentSettingPopupTab)}
|
onSelect={({ key }) => setMenu(key as AgentSettingPopupTab)}
|
||||||
/>
|
/>
|
||||||
@ -74,7 +82,7 @@ const AgentSettingPopupContainer: React.FC<AgentSettingPopupParams> = ({ tab, ag
|
|||||||
{menu === 'essential' && <AgentEssentialSettings agent={agent} update={updateAgent} />}
|
{menu === 'essential' && <AgentEssentialSettings agent={agent} update={updateAgent} />}
|
||||||
{menu === 'prompt' && <AgentPromptSettings agent={agent} update={updateAgent} />}
|
{menu === 'prompt' && <AgentPromptSettings agent={agent} update={updateAgent} />}
|
||||||
</Settings>
|
</Settings>
|
||||||
</HStack>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -98,15 +106,19 @@ const AgentSettingPopupContainer: React.FC<AgentSettingPopupParams> = ({ tab, ag
|
|||||||
styles={{
|
styles={{
|
||||||
content: {
|
content: {
|
||||||
padding: 0,
|
padding: 0,
|
||||||
overflow: 'hidden'
|
overflow: 'hidden',
|
||||||
|
height: '80vh',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column'
|
||||||
},
|
},
|
||||||
header: { padding: '10px 15px', borderBottom: '0.5px solid var(--color-border)', margin: 0, borderRadius: 0 },
|
header: { padding: '10px 15px', borderBottom: '0.5px solid var(--color-border)', margin: 0, borderRadius: 0 },
|
||||||
body: {
|
body: {
|
||||||
padding: 0
|
padding: 0,
|
||||||
|
display: 'flex',
|
||||||
|
flex: 1
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
width="min(800px, 70vw)"
|
width="min(800px, 70vw)"
|
||||||
height="80vh"
|
|
||||||
centered>
|
centered>
|
||||||
<ModalContent />
|
<ModalContent />
|
||||||
</StyledModal>
|
</StyledModal>
|
||||||
@ -114,7 +126,7 @@ const AgentSettingPopupContainer: React.FC<AgentSettingPopupParams> = ({ tab, ag
|
|||||||
}
|
}
|
||||||
|
|
||||||
const LeftMenu = styled.div`
|
const LeftMenu = styled.div`
|
||||||
height: calc(80vh - 20px);
|
height: 100%;
|
||||||
border-right: 0.5px solid var(--color-border);
|
border-right: 0.5px solid var(--color-border);
|
||||||
`
|
`
|
||||||
|
|
||||||
@ -123,7 +135,6 @@ const Settings = styled.div`
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
padding: 16px 16px;
|
padding: 16px 16px;
|
||||||
height: calc(80vh - 16px);
|
|
||||||
overflow-y: scroll;
|
overflow-y: scroll;
|
||||||
`
|
`
|
||||||
|
|
||||||
|
|||||||
@ -2,12 +2,21 @@ import { Avatar, AvatarProps, cn } from '@heroui/react'
|
|||||||
import { getAgentAvatar } from '@renderer/config/agent'
|
import { getAgentAvatar } from '@renderer/config/agent'
|
||||||
import { getAgentTypeLabel } from '@renderer/i18n/label'
|
import { getAgentTypeLabel } from '@renderer/i18n/label'
|
||||||
import { AgentType } from '@renderer/types'
|
import { AgentType } from '@renderer/types'
|
||||||
import React from 'react'
|
import React, { ReactNode } from 'react'
|
||||||
|
|
||||||
import { SettingDivider } from '..'
|
import { SettingDivider } from '..'
|
||||||
|
|
||||||
export const SettingsTitle: React.FC<React.PropsWithChildren> = ({ children }) => {
|
export interface SettingsTitleProps extends React.ComponentPropsWithRef<'div'> {
|
||||||
return <div className="mb-1 flex items-center gap-2 font-bold">{children}</div>
|
actions?: ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SettingsTitle: React.FC<SettingsTitleProps> = ({ children, actions }) => {
|
||||||
|
return (
|
||||||
|
<div className={cn(actions ? 'justify-between' : undefined, 'mb-1 flex items-center gap-2')}>
|
||||||
|
<span className="flex items-center gap-1 font-bold">{children}</span>
|
||||||
|
{actions !== undefined && actions}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export type AgentLabelProps = {
|
export type AgentLabelProps = {
|
||||||
|
|||||||
@ -57,7 +57,7 @@ export const AgentBaseSchema = z.object({
|
|||||||
// Basic info
|
// Basic info
|
||||||
name: z.string().optional(),
|
name: z.string().optional(),
|
||||||
description: z.string().optional(),
|
description: z.string().optional(),
|
||||||
accessible_paths: z.array(z.string()), // Array of directory paths the agent can access
|
accessible_paths: z.array(z.string()).nonempty(), // Array of directory paths the agent can access
|
||||||
|
|
||||||
// Instructions for the agent
|
// Instructions for the agent
|
||||||
instructions: z.string().optional(), // System prompt
|
instructions: z.string().optional(), // System prompt
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user