fix: plugin setting related

This commit is contained in:
dev 2025-11-05 12:20:36 +08:00
parent 81fecce552
commit 47db5baeb1
2 changed files with 166 additions and 88 deletions

View File

@ -1,7 +1,8 @@
import { Card, CardBody, Tab, Tabs } from '@heroui/react'
import { useAvailablePlugins, useInstalledPlugins, usePluginActions } from '@renderer/hooks/usePlugins'
import type { GetAgentResponse, GetAgentSessionResponse, UpdateAgentFunctionUnion } from '@renderer/types/agent'
import { Card, Tabs } from 'antd'
import type { FC } from 'react'
import { useMemo } from 'react'
import { useCallback } from 'react'
import { useTranslation } from 'react-i18next'
@ -54,24 +55,18 @@ const PluginSettings: FC<PluginSettingsProps> = ({ agentBase }) => {
[uninstall, t]
)
return (
<SettingsContainer className="pr-0">
<Tabs
aria-label="Plugin settings tabs"
classNames={{
base: 'w-full',
tabList: 'w-full',
panel: 'w-full flex-1 overflow-hidden'
}}>
<Tab key="available" title={t('agent.settings.plugins.available.title')}>
const tabItems = useMemo(() => {
return [
{
key: 'available',
label: t('agent.settings.plugins.available.title'),
children: (
<div className="flex h-full flex-col overflow-y-auto pt-1 pr-2">
{errorAvailable ? (
<Card className="bg-danger-50 dark:bg-danger-900/20">
<CardBody>
<Card variant="borderless">
<p className="text-danger">
{t('agent.settings.plugins.error.load')}: {errorAvailable}
</p>
</CardBody>
</Card>
) : (
<PluginBrowser
@ -86,17 +81,18 @@ const PluginSettings: FC<PluginSettingsProps> = ({ agentBase }) => {
/>
)}
</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">
{errorInstalled ? (
<Card className="bg-danger-50 dark:bg-danger-900/20">
<CardBody>
<p className="text-danger">
{t('agent.settings.plugins.error.load')}: {errorInstalled}
</p>
</CardBody>
</Card>
) : (
<InstalledPluginsList
@ -106,8 +102,29 @@ const PluginSettings: FC<PluginSettingsProps> = ({ agentBase }) => {
/>
)}
</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>
)
}

View File

@ -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 { 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 type { FC } 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 [isModalOpen, setIsModalOpen] = useState(false)
const observerTarget = useRef<HTMLDivElement>(null)
const [filterDropdownOpen, setFilterDropdownOpen] = useState(false)
// Combine all plugins based on active type
const allPlugins = useMemo(() => {
@ -92,6 +94,68 @@ export const PluginBrowser: FC<PluginBrowserProps> = ({
return filteredPlugins.slice(0, 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
// Reset display count when filters change
@ -166,77 +230,74 @@ export const PluginBrowser: FC<PluginBrowserProps> = ({
setSelectedPlugin(null)
}
const handleOpenFilterDropdown = () => {
setFilterDropdownOpen(!filterDropdownOpen)
}
return (
<div className="flex flex-col gap-4">
{/* Search and Filter */}
<div className="relative flex gap-0">
<Input
<AntInput
placeholder={t('plugins.search_placeholder')}
value={searchQuery}
onValueChange={handleSearchChange}
startContent={<Search className="h-4 w-4 text-default-400" />}
isClearable
size="md"
className="flex-1"
classNames={{
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'}
onChange={(e) => handleSearchChange(e.target.value)}
prefix={<Search className="h-4 w-4 text-default-400" />}
size="large"
suffix={
<AntButton
variant={selectedCategories.length > 0 ? 'filled' : 'text'}
color={selectedCategories.length > 0 ? 'primary' : 'default'}
size="sm"
className="-translate-y-1/2 absolute top-1/2 right-2 z-10">
<Filter className="h-4 w-4" />
</Button>
</DropdownTrigger>
<DropdownMenu
aria-label="Category filter"
closeOnSelect={false}
className="max-h-60 overflow-y-auto"
items={[
{ key: 'all', label: t('plugins.all_categories') },
...allCategories.map((category) => ({ key: category, label: category }))
]}>
{(item) => {
const isSelected =
item.key === 'all' ? selectedCategories.length === 0 : selectedCategories.includes(item.key)
return (
<DropdownItem
key={item.key}
textValue={item.label}
onPress={() => {
if (item.key === 'all') {
handleCategoryChange(new Set(['all']))
} else {
const newKeys = selectedCategories.includes(item.key)
? new Set(selectedCategories.filter((c) => c !== item.key))
: new Set([...selectedCategories, item.key])
handleCategoryChange(newKeys)
size="small"
style={{
position: 'absolute',
top: '50%',
right: '8px',
transform: 'translateY(-50%)',
padding: '4px',
minWidth: '32px',
borderRadius: '4px'
}}
icon={<Filter className="h-4 w-4" />}
onClick={handleOpenFilterDropdown}
/>
}
/>
<AntDropdown
menu={{ items: pluginCategoryMenuItems }}
trigger={['click']}
open={filterDropdownOpen}
onOpenChange={(nextOpen) => {
setFilterDropdownOpen(nextOpen)
}}>
<AntButton
variant={selectedCategories.length > 0 ? 'filled' : 'text'}
color={selectedCategories.length > 0 ? 'primary' : 'default'}
size="small"
style={{
position: 'absolute',
top: '50%',
right: '8px',
transform: 'translateY(-50%)',
padding: '4px',
minWidth: '32px',
borderRadius: '4px'
}}
className={isSelected ? 'bg-primary-50' : ''}>
{item.label}
{isSelected && <span className="ml-2 text-primary text-sm"></span>}
</DropdownItem>
)
}}
</DropdownMenu>
</Dropdown>
icon={<Filter className="h-4 w-4" />}
/>
</AntDropdown>
</div>
{/* Type Tabs */}
<div className="-mt-3 flex justify-center">
<Tabs selectedKey={activeType} onSelectionChange={handleTypeChange} variant="underlined">
<Tab key="all" title={t('plugins.all_types')} />
<Tab key="agent" title={t('plugins.agents')} />
<Tab key="command" title={t('plugins.commands')} />
<Tab key="skill" title={t('plugins.skills')} />
</Tabs>
<div className="-mb-3 flex w-full justify-center">
<AntTabs
activeKey={activeType}
onChange={handleTypeChange}
items={pluginTypeTabItems}
className="w-full"
size="small"
centered
/>
</div>
{/* Result Count */}