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 { 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>
) )
} }

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 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)
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)
} }
/>
<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' : ''}> icon={<Filter className="h-4 w-4" />}
{item.label} />
{isSelected && <span className="ml-2 text-primary text-sm"></span>} </AntDropdown>
</DropdownItem>
)
}}
</DropdownMenu>
</Dropdown>
</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 */}