mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2025-12-23 18:10:26 +08:00
fix: plugin setting related
This commit is contained in:
parent
81fecce552
commit
47db5baeb1
@ -1,7 +1,8 @@
|
|||||||
import { Card, CardBody, Tab, Tabs } from '@heroui/react'
|
|
||||||
import { useAvailablePlugins, useInstalledPlugins, usePluginActions } from '@renderer/hooks/usePlugins'
|
import { useAvailablePlugins, useInstalledPlugins, usePluginActions } from '@renderer/hooks/usePlugins'
|
||||||
import type { GetAgentResponse, GetAgentSessionResponse, UpdateAgentFunctionUnion } from '@renderer/types/agent'
|
import type { GetAgentResponse, GetAgentSessionResponse, UpdateAgentFunctionUnion } from '@renderer/types/agent'
|
||||||
|
import { Card, Tabs } from 'antd'
|
||||||
import type { FC } from 'react'
|
import type { FC } from 'react'
|
||||||
|
import { useMemo } from 'react'
|
||||||
import { useCallback } from 'react'
|
import { useCallback } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
@ -54,24 +55,18 @@ const PluginSettings: FC<PluginSettingsProps> = ({ agentBase }) => {
|
|||||||
[uninstall, t]
|
[uninstall, t]
|
||||||
)
|
)
|
||||||
|
|
||||||
return (
|
const tabItems = useMemo(() => {
|
||||||
<SettingsContainer className="pr-0">
|
return [
|
||||||
<Tabs
|
{
|
||||||
aria-label="Plugin settings tabs"
|
key: 'available',
|
||||||
classNames={{
|
label: t('agent.settings.plugins.available.title'),
|
||||||
base: 'w-full',
|
children: (
|
||||||
tabList: 'w-full',
|
|
||||||
panel: 'w-full flex-1 overflow-hidden'
|
|
||||||
}}>
|
|
||||||
<Tab key="available" title={t('agent.settings.plugins.available.title')}>
|
|
||||||
<div className="flex h-full flex-col overflow-y-auto pt-1 pr-2">
|
<div className="flex h-full flex-col overflow-y-auto pt-1 pr-2">
|
||||||
{errorAvailable ? (
|
{errorAvailable ? (
|
||||||
<Card className="bg-danger-50 dark:bg-danger-900/20">
|
<Card variant="borderless">
|
||||||
<CardBody>
|
<p className="text-danger">
|
||||||
<p className="text-danger">
|
{t('agent.settings.plugins.error.load')}: {errorAvailable}
|
||||||
{t('agent.settings.plugins.error.load')}: {errorAvailable}
|
</p>
|
||||||
</p>
|
|
||||||
</CardBody>
|
|
||||||
</Card>
|
</Card>
|
||||||
) : (
|
) : (
|
||||||
<PluginBrowser
|
<PluginBrowser
|
||||||
@ -86,17 +81,18 @@ const PluginSettings: FC<PluginSettingsProps> = ({ agentBase }) => {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</Tab>
|
)
|
||||||
|
},
|
||||||
<Tab key="installed" title={t('agent.settings.plugins.installed.title')}>
|
{
|
||||||
|
key: 'installed',
|
||||||
|
label: t('agent.settings.plugins.installed.title'),
|
||||||
|
children: (
|
||||||
<div className="flex h-full flex-col overflow-y-auto pt-4 pr-2">
|
<div className="flex h-full flex-col overflow-y-auto pt-4 pr-2">
|
||||||
{errorInstalled ? (
|
{errorInstalled ? (
|
||||||
<Card className="bg-danger-50 dark:bg-danger-900/20">
|
<Card className="bg-danger-50 dark:bg-danger-900/20">
|
||||||
<CardBody>
|
<p className="text-danger">
|
||||||
<p className="text-danger">
|
{t('agent.settings.plugins.error.load')}: {errorInstalled}
|
||||||
{t('agent.settings.plugins.error.load')}: {errorInstalled}
|
</p>
|
||||||
</p>
|
|
||||||
</CardBody>
|
|
||||||
</Card>
|
</Card>
|
||||||
) : (
|
) : (
|
||||||
<InstalledPluginsList
|
<InstalledPluginsList
|
||||||
@ -106,8 +102,29 @@ const PluginSettings: FC<PluginSettingsProps> = ({ agentBase }) => {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</Tab>
|
)
|
||||||
</Tabs>
|
}
|
||||||
|
]
|
||||||
|
}, [
|
||||||
|
agentBase.id,
|
||||||
|
agents,
|
||||||
|
commands,
|
||||||
|
errorAvailable,
|
||||||
|
errorInstalled,
|
||||||
|
handleInstall,
|
||||||
|
handleUninstall,
|
||||||
|
installing,
|
||||||
|
loadingAvailable,
|
||||||
|
loadingInstalled,
|
||||||
|
plugins,
|
||||||
|
skills,
|
||||||
|
t,
|
||||||
|
uninstalling
|
||||||
|
])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SettingsContainer className="pr-0">
|
||||||
|
<Tabs centered items={tabItems} />
|
||||||
</SettingsContainer>
|
</SettingsContainer>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import { Button, Dropdown, DropdownItem, DropdownMenu, DropdownTrigger, Input, Tab, Tabs } from '@heroui/react'
|
|
||||||
import type { InstalledPlugin, PluginMetadata } from '@renderer/types/plugin'
|
import type { InstalledPlugin, PluginMetadata } from '@renderer/types/plugin'
|
||||||
|
import { Button as AntButton, Dropdown as AntDropdown, Input as AntInput, Tabs as AntTabs } from 'antd'
|
||||||
|
import type { ItemType } from 'antd/es/menu/interface'
|
||||||
import { Filter, Search } from 'lucide-react'
|
import { Filter, Search } from 'lucide-react'
|
||||||
import type { FC } from 'react'
|
import type { FC } from 'react'
|
||||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||||
@ -42,6 +43,7 @@ export const PluginBrowser: FC<PluginBrowserProps> = ({
|
|||||||
const [selectedPlugin, setSelectedPlugin] = useState<PluginMetadata | null>(null)
|
const [selectedPlugin, setSelectedPlugin] = useState<PluginMetadata | null>(null)
|
||||||
const [isModalOpen, setIsModalOpen] = useState(false)
|
const [isModalOpen, setIsModalOpen] = useState(false)
|
||||||
const observerTarget = useRef<HTMLDivElement>(null)
|
const observerTarget = useRef<HTMLDivElement>(null)
|
||||||
|
const [filterDropdownOpen, setFilterDropdownOpen] = useState(false)
|
||||||
|
|
||||||
// Combine all plugins based on active type
|
// Combine all plugins based on active type
|
||||||
const allPlugins = useMemo(() => {
|
const allPlugins = useMemo(() => {
|
||||||
@ -92,6 +94,68 @@ export const PluginBrowser: FC<PluginBrowserProps> = ({
|
|||||||
return filteredPlugins.slice(0, displayCount)
|
return filteredPlugins.slice(0, displayCount)
|
||||||
}, [filteredPlugins, displayCount])
|
}, [filteredPlugins, displayCount])
|
||||||
|
|
||||||
|
const pluginCategoryMenuItems = useMemo(() => {
|
||||||
|
const isSelected = (category: string): boolean =>
|
||||||
|
category === 'all' ? selectedCategories.length === 0 : selectedCategories.includes(category)
|
||||||
|
const handleClick = (category: string) => {
|
||||||
|
if (category === 'all') {
|
||||||
|
handleCategoryChange(new Set(['all']))
|
||||||
|
} else {
|
||||||
|
const newKeys = selectedCategories.includes(category)
|
||||||
|
? new Set(selectedCategories.filter((c) => c !== category))
|
||||||
|
: new Set([...selectedCategories, category])
|
||||||
|
handleCategoryChange(newKeys)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const itemLabel = (category: string) => (
|
||||||
|
<div className="flex flex-row justify-between">
|
||||||
|
{category}
|
||||||
|
{isSelected(category) && <span className="ml-2 text-primary text-sm">✓</span>}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
key: 'all',
|
||||||
|
title: t('plugins.all_categories'),
|
||||||
|
label: itemLabel('all'),
|
||||||
|
onClick: () => handleClick('all')
|
||||||
|
},
|
||||||
|
...allCategories.map(
|
||||||
|
(category) =>
|
||||||
|
({
|
||||||
|
key: category,
|
||||||
|
title: category,
|
||||||
|
label: itemLabel(category),
|
||||||
|
onClick: () => handleClick(category)
|
||||||
|
}) satisfies ItemType
|
||||||
|
)
|
||||||
|
]
|
||||||
|
}, [allCategories, selectedCategories, t])
|
||||||
|
|
||||||
|
const pluginTypeTabItems = useMemo(
|
||||||
|
() => [
|
||||||
|
{
|
||||||
|
key: 'all',
|
||||||
|
label: t('plugins.all_types')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'agent',
|
||||||
|
label: t('plugins.agents')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'command',
|
||||||
|
label: t('plugins.commands')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'skill',
|
||||||
|
label: t('plugins.skills')
|
||||||
|
}
|
||||||
|
],
|
||||||
|
[t]
|
||||||
|
)
|
||||||
|
|
||||||
const hasMore = displayCount < filteredPlugins.length
|
const hasMore = displayCount < filteredPlugins.length
|
||||||
|
|
||||||
// Reset display count when filters change
|
// Reset display count when filters change
|
||||||
@ -166,77 +230,74 @@ export const PluginBrowser: FC<PluginBrowserProps> = ({
|
|||||||
setSelectedPlugin(null)
|
setSelectedPlugin(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleOpenFilterDropdown = () => {
|
||||||
|
setFilterDropdownOpen(!filterDropdownOpen)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
{/* Search and Filter */}
|
{/* Search and Filter */}
|
||||||
<div className="relative flex gap-0">
|
<div className="relative flex gap-0">
|
||||||
<Input
|
<AntInput
|
||||||
placeholder={t('plugins.search_placeholder')}
|
placeholder={t('plugins.search_placeholder')}
|
||||||
value={searchQuery}
|
value={searchQuery}
|
||||||
onValueChange={handleSearchChange}
|
onChange={(e) => handleSearchChange(e.target.value)}
|
||||||
startContent={<Search className="h-4 w-4 text-default-400" />}
|
prefix={<Search className="h-4 w-4 text-default-400" />}
|
||||||
isClearable
|
size="large"
|
||||||
size="md"
|
suffix={
|
||||||
className="flex-1"
|
<AntButton
|
||||||
classNames={{
|
variant={selectedCategories.length > 0 ? 'filled' : 'text'}
|
||||||
inputWrapper: 'pr-12'
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Dropdown placement="bottom-end" classNames={{ content: 'max-h-60 overflow-y-auto p-0' }}>
|
|
||||||
<DropdownTrigger>
|
|
||||||
<Button
|
|
||||||
isIconOnly
|
|
||||||
variant={selectedCategories.length > 0 ? 'flat' : 'light'}
|
|
||||||
color={selectedCategories.length > 0 ? 'primary' : 'default'}
|
color={selectedCategories.length > 0 ? 'primary' : 'default'}
|
||||||
size="sm"
|
size="small"
|
||||||
className="-translate-y-1/2 absolute top-1/2 right-2 z-10">
|
style={{
|
||||||
<Filter className="h-4 w-4" />
|
position: 'absolute',
|
||||||
</Button>
|
top: '50%',
|
||||||
</DropdownTrigger>
|
right: '8px',
|
||||||
<DropdownMenu
|
transform: 'translateY(-50%)',
|
||||||
aria-label="Category filter"
|
padding: '4px',
|
||||||
closeOnSelect={false}
|
minWidth: '32px',
|
||||||
className="max-h-60 overflow-y-auto"
|
borderRadius: '4px'
|
||||||
items={[
|
}}
|
||||||
{ key: 'all', label: t('plugins.all_categories') },
|
icon={<Filter className="h-4 w-4" />}
|
||||||
...allCategories.map((category) => ({ key: category, label: category }))
|
onClick={handleOpenFilterDropdown}
|
||||||
]}>
|
/>
|
||||||
{(item) => {
|
}
|
||||||
const isSelected =
|
/>
|
||||||
item.key === 'all' ? selectedCategories.length === 0 : selectedCategories.includes(item.key)
|
<AntDropdown
|
||||||
|
menu={{ items: pluginCategoryMenuItems }}
|
||||||
return (
|
trigger={['click']}
|
||||||
<DropdownItem
|
open={filterDropdownOpen}
|
||||||
key={item.key}
|
onOpenChange={(nextOpen) => {
|
||||||
textValue={item.label}
|
setFilterDropdownOpen(nextOpen)
|
||||||
onPress={() => {
|
}}>
|
||||||
if (item.key === 'all') {
|
<AntButton
|
||||||
handleCategoryChange(new Set(['all']))
|
variant={selectedCategories.length > 0 ? 'filled' : 'text'}
|
||||||
} else {
|
color={selectedCategories.length > 0 ? 'primary' : 'default'}
|
||||||
const newKeys = selectedCategories.includes(item.key)
|
size="small"
|
||||||
? new Set(selectedCategories.filter((c) => c !== item.key))
|
style={{
|
||||||
: new Set([...selectedCategories, item.key])
|
position: 'absolute',
|
||||||
handleCategoryChange(newKeys)
|
top: '50%',
|
||||||
}
|
right: '8px',
|
||||||
}}
|
transform: 'translateY(-50%)',
|
||||||
className={isSelected ? 'bg-primary-50' : ''}>
|
padding: '4px',
|
||||||
{item.label}
|
minWidth: '32px',
|
||||||
{isSelected && <span className="ml-2 text-primary text-sm">✓</span>}
|
borderRadius: '4px'
|
||||||
</DropdownItem>
|
|
||||||
)
|
|
||||||
}}
|
}}
|
||||||
</DropdownMenu>
|
icon={<Filter className="h-4 w-4" />}
|
||||||
</Dropdown>
|
/>
|
||||||
|
</AntDropdown>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Type Tabs */}
|
{/* Type Tabs */}
|
||||||
<div className="-mt-3 flex justify-center">
|
<div className="-mb-3 flex w-full justify-center">
|
||||||
<Tabs selectedKey={activeType} onSelectionChange={handleTypeChange} variant="underlined">
|
<AntTabs
|
||||||
<Tab key="all" title={t('plugins.all_types')} />
|
activeKey={activeType}
|
||||||
<Tab key="agent" title={t('plugins.agents')} />
|
onChange={handleTypeChange}
|
||||||
<Tab key="command" title={t('plugins.commands')} />
|
items={pluginTypeTabItems}
|
||||||
<Tab key="skill" title={t('plugins.skills')} />
|
className="w-full"
|
||||||
</Tabs>
|
size="small"
|
||||||
|
centered
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Result Count */}
|
{/* Result Count */}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user