mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2026-01-07 05:39:05 +08:00
feat(agent): add AllowedToolsSelect component and integrate into forms
extract tool selection logic into reusable component to reduce code duplication and improve maintainability
This commit is contained in:
parent
73895b5f4b
commit
d4d2510834
@ -1,6 +1,5 @@
|
||||
import {
|
||||
Button,
|
||||
Chip,
|
||||
cn,
|
||||
Form,
|
||||
Input,
|
||||
@ -18,6 +17,7 @@ import {
|
||||
} from '@heroui/react'
|
||||
import { loggerService } from '@logger'
|
||||
import type { Selection } from '@react-types/shared'
|
||||
import { AllowedToolsSelect } from '@renderer/components/agent'
|
||||
import { getModelLogo } from '@renderer/config/models'
|
||||
import { useAgent } from '@renderer/hooks/agents/useAgent'
|
||||
import { useApiModels } from '@renderer/hooks/agents/useModels'
|
||||
@ -197,21 +197,6 @@ export const SessionModal: React.FC<Props> = ({
|
||||
[availableTools]
|
||||
)
|
||||
|
||||
const renderSelectedTools = useCallback((items: SelectedItems<Tool>) => {
|
||||
if (!items.length) {
|
||||
return null
|
||||
}
|
||||
return (
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{items.map((item) => (
|
||||
<Chip key={item.key} size="sm" variant="flat" className="max-w-[160px] truncate">
|
||||
{item.data?.name ?? item.textValue ?? item.key}
|
||||
</Chip>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}, [])
|
||||
|
||||
const modelOptions = useMemo(() => {
|
||||
// mocked data. not final version
|
||||
return (models ?? []).map((model) => ({
|
||||
@ -359,32 +344,11 @@ export const SessionModal: React.FC<Props> = ({
|
||||
value={form.description ?? ''}
|
||||
onValueChange={onDescChange}
|
||||
/>
|
||||
<Select
|
||||
selectionMode="multiple"
|
||||
isMultiline
|
||||
<AllowedToolsSelect
|
||||
items={availableTools}
|
||||
selectedKeys={selectedToolKeys}
|
||||
onSelectionChange={onAllowedToolsChange}
|
||||
label={t('agent.session.allowed_tools.label')}
|
||||
placeholder={t('agent.session.allowed_tools.placeholder')}
|
||||
description={
|
||||
availableTools.length
|
||||
? t('agent.session.allowed_tools.helper')
|
||||
: t('agent.session.allowed_tools.empty')
|
||||
}
|
||||
isDisabled={!availableTools.length}
|
||||
items={availableTools}
|
||||
renderValue={renderSelectedTools}>
|
||||
{(tool) => (
|
||||
<SelectItem key={tool.id} textValue={tool.name}>
|
||||
<div className="flex flex-col">
|
||||
<span className="font-medium text-sm">{tool.name}</span>
|
||||
{tool.description ? (
|
||||
<span className="text-foreground-500 text-xs">{tool.description}</span>
|
||||
) : null}
|
||||
</div>
|
||||
</SelectItem>
|
||||
)}
|
||||
</Select>
|
||||
/>
|
||||
<Textarea label={t('common.prompt')} value={form.instructions ?? ''} onValueChange={onInstChange} />
|
||||
</ModalBody>
|
||||
<ModalFooter className="w-full">
|
||||
|
||||
54
src/renderer/src/components/agent/AllowedToolsSelect.tsx
Normal file
54
src/renderer/src/components/agent/AllowedToolsSelect.tsx
Normal file
@ -0,0 +1,54 @@
|
||||
import { Chip, cn, Select, SelectedItems, SelectItem, SelectProps } from '@heroui/react'
|
||||
import { Tool } from '@renderer/types'
|
||||
import React, { useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
export interface AllowedToolsSelectProps extends Omit<SelectProps, 'children'> {
|
||||
items: Tool[]
|
||||
}
|
||||
|
||||
export const AllowedToolsSelect: React.FC<AllowedToolsSelectProps> = (props) => {
|
||||
const { t } = useTranslation()
|
||||
const { items: availableTools, className, ...rest } = props
|
||||
|
||||
const renderSelectedTools = useCallback((items: SelectedItems<Tool>) => {
|
||||
if (!items.length) {
|
||||
return null
|
||||
}
|
||||
return (
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{items.map((item) => (
|
||||
<Chip key={item.key} size="sm" variant="flat" className="max-w-[160px] truncate">
|
||||
{item.data?.name ?? item.textValue ?? item.key}
|
||||
</Chip>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<Select
|
||||
aria-label={t('agent.session.allowed_tools.label')}
|
||||
selectionMode="multiple"
|
||||
isMultiline
|
||||
label={t('agent.session.allowed_tools.label')}
|
||||
placeholder={t('agent.session.allowed_tools.placeholder')}
|
||||
description={
|
||||
availableTools.length ? t('agent.session.allowed_tools.helper') : t('agent.session.allowed_tools.empty')
|
||||
}
|
||||
isDisabled={!availableTools.length}
|
||||
items={availableTools}
|
||||
renderValue={renderSelectedTools}
|
||||
className={cn('max-w-xl', className)}
|
||||
{...rest}>
|
||||
{(tool) => (
|
||||
<SelectItem key={tool.id} textValue={tool.name}>
|
||||
<div className="flex flex-col">
|
||||
<span className="font-medium text-sm">{tool.name}</span>
|
||||
{tool.description ? <span className="text-foreground-500 text-xs">{tool.description}</span> : null}
|
||||
</div>
|
||||
</SelectItem>
|
||||
)}
|
||||
</Select>
|
||||
)
|
||||
}
|
||||
1
src/renderer/src/components/agent/index.tsx
Normal file
1
src/renderer/src/components/agent/index.tsx
Normal file
@ -0,0 +1 @@
|
||||
export { AllowedToolsSelect } from './AllowedToolsSelect'
|
||||
@ -1,10 +1,11 @@
|
||||
import { Button, Chip, Select as HeroSelect, SelectedItems, SelectItem, Tooltip } from '@heroui/react'
|
||||
import { Button, Tooltip } from '@heroui/react'
|
||||
import { loggerService } from '@logger'
|
||||
import type { Selection } from '@react-types/shared'
|
||||
import { AllowedToolsSelect } from '@renderer/components/agent'
|
||||
import { ApiModelLabel } from '@renderer/components/ApiModelLabel'
|
||||
import { useApiModels } from '@renderer/hooks/agents/useModels'
|
||||
import { useUpdateAgent } from '@renderer/hooks/agents/useUpdateAgent'
|
||||
import { GetAgentResponse, Tool, UpdateAgentForm } from '@renderer/types'
|
||||
import { GetAgentResponse, UpdateAgentForm } from '@renderer/types'
|
||||
import { Input, Select } from 'antd'
|
||||
import { DefaultOptionType } from 'antd/es/select'
|
||||
import { Plus } from 'lucide-react'
|
||||
@ -116,22 +117,6 @@ const AgentEssentialSettings: FC<AgentEssentialSettingsProps> = ({ agent, update
|
||||
[agent, t, updateAccessiblePaths]
|
||||
)
|
||||
|
||||
const renderSelectedTools = useCallback((items: SelectedItems<Tool>) => {
|
||||
if (!items.length) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{items.map((item) => (
|
||||
<Chip key={item.key} size="sm" variant="flat" className="max-w-[160px] truncate">
|
||||
{item.data?.name ?? item.textValue ?? item.key}
|
||||
</Chip>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}, [])
|
||||
|
||||
const onAllowedToolsChange = useCallback(
|
||||
(keys: Selection) => {
|
||||
if (!agent) return
|
||||
@ -149,20 +134,23 @@ const AgentEssentialSettings: FC<AgentEssentialSettingsProps> = ({ agent, update
|
||||
}
|
||||
return filtered
|
||||
})
|
||||
|
||||
const previous = agent.allowed_tools ?? []
|
||||
const previousSet = new Set(previous)
|
||||
const isSameSelection = filtered.length === previousSet.size && filtered.every((id) => previousSet.has(id))
|
||||
|
||||
if (isSameSelection) {
|
||||
return
|
||||
}
|
||||
|
||||
updateAllowedTools(filtered)
|
||||
},
|
||||
[agent, availableTools, updateAllowedTools]
|
||||
[agent, availableTools]
|
||||
)
|
||||
|
||||
const onAllowedToolsSelected = useCallback(() => {
|
||||
if (!agent) return
|
||||
const previous = agent.allowed_tools ?? []
|
||||
const previousSet = new Set(previous)
|
||||
const isSameSelection =
|
||||
allowedToolIds.length === previousSet.size && allowedToolIds.every((id) => previousSet.has(id))
|
||||
|
||||
if (isSameSelection) {
|
||||
return
|
||||
}
|
||||
updateAllowedTools(allowedToolIds)
|
||||
}, [agent, allowedToolIds, updateAllowedTools])
|
||||
|
||||
if (!agent) return null
|
||||
|
||||
return (
|
||||
@ -199,28 +187,12 @@ const AgentEssentialSettings: FC<AgentEssentialSettingsProps> = ({ agent, update
|
||||
</SettingsItem>
|
||||
<SettingsItem>
|
||||
<SettingsTitle>{t('agent.session.allowed_tools.label')}</SettingsTitle>
|
||||
<HeroSelect
|
||||
aria-label={t('agent.session.allowed_tools.label')}
|
||||
selectionMode="multiple"
|
||||
<AllowedToolsSelect
|
||||
items={availableTools}
|
||||
selectedKeys={selectedToolKeys}
|
||||
onSelectionChange={onAllowedToolsChange}
|
||||
placeholder={t('agent.session.allowed_tools.placeholder')}
|
||||
description={
|
||||
availableTools.length ? t('agent.session.allowed_tools.helper') : t('agent.session.allowed_tools.empty')
|
||||
}
|
||||
isDisabled={!availableTools.length}
|
||||
items={availableTools}
|
||||
renderValue={renderSelectedTools}
|
||||
className="max-w-xl">
|
||||
{(tool) => (
|
||||
<SelectItem key={tool.id} textValue={tool.name}>
|
||||
<div className="flex flex-col">
|
||||
<span className="font-medium text-sm">{tool.name}</span>
|
||||
{tool.description ? <span className="text-foreground-500 text-xs">{tool.description}</span> : null}
|
||||
</div>
|
||||
</SelectItem>
|
||||
)}
|
||||
</HeroSelect>
|
||||
onClose={onAllowedToolsSelected}
|
||||
/>
|
||||
</SettingsItem>
|
||||
<SettingsItem>
|
||||
<SettingsTitle
|
||||
|
||||
Loading…
Reference in New Issue
Block a user