fix: minor ui tweak of plugin installation interface (#11085)

* fix: use dropdown instead of chip filter

* fix: add padding to avoid scroll bar overlap

* fix: set max card grid col to 2

* fix: minor ui tweak for plugin card

* fix: remove redundant args

* fix(i18n): Auto update translations for PR #11085

* fix: cleanup comments

---------

Co-authored-by: GitHub Action <action@github.com>
This commit is contained in:
defi-failure 2025-10-31 22:28:25 +08:00 committed by GitHub
parent 1018ad87b8
commit 562fbb3ff7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 121 additions and 110 deletions

View File

@ -3052,10 +3052,10 @@
"connection_failed": "Falha na conexão", "connection_failed": "Falha na conexão",
"content": "Certifique-se de que o computador e o telefone estejam na mesma rede para usar a transferência via LAN. Abra o aplicativo Cherry Studio e escaneie este código QR.", "content": "Certifique-se de que o computador e o telefone estejam na mesma rede para usar a transferência via LAN. Abra o aplicativo Cherry Studio e escaneie este código QR.",
"error": { "error": {
"init_failed": "[to be translated]:Initialization failed", "init_failed": "Falha na inicialização",
"no_file": "Nenhum arquivo selecionado", "no_file": "Nenhum arquivo selecionado",
"no_ip": "[to be translated]:Unable to get IP address", "no_ip": "Incapaz de obter endereço IP",
"send_failed": "[to be translated]:Failed to send file" "send_failed": "Falha ao enviar arquivo"
}, },
"force_close": "Forçar Fechamento", "force_close": "Forçar Fechamento",
"generating_qr": "Gerando código QR...", "generating_qr": "Gerando código QR...",
@ -3064,15 +3064,15 @@
"selectZip": "Selecionar arquivo compactado", "selectZip": "Selecionar arquivo compactado",
"sendZip": "Iniciar recuperação de dados", "sendZip": "Iniciar recuperação de dados",
"status": { "status": {
"completed": "[to be translated]:Transfer completed", "completed": "Transferência concluída",
"connected": "[to be translated]:Connected", "connected": "Conectado",
"connecting": "[to be translated]:Connecting...", "connecting": "Conectando...",
"disconnected": "[to be translated]:Disconnected", "disconnected": "Desconectado",
"error": "[to be translated]:Connection error", "error": "Erro de conexão",
"initializing": "[to be translated]:Initializing connection...", "initializing": "Inicializando conexão...",
"preparing": "[to be translated]:Preparing transfer...", "preparing": "Preparando transferência...",
"sending": "[to be translated]:Transferring {{progress}}%", "sending": "Transferindo {{progress}}%",
"waiting_qr_scan": "[to be translated]:Please scan QR code to connect" "waiting_qr_scan": "Por favor, escaneie o código QR para conectar"
}, },
"title": "transmissão de rede local", "title": "transmissão de rede local",
"transfer_progress": "Progresso da transferência" "transfer_progress": "Progresso da transferência"

View File

@ -1047,7 +1047,7 @@
"clear": "Очистить", "clear": "Очистить",
"close": "Закрыть", "close": "Закрыть",
"collapse": "Свернуть", "collapse": "Свернуть",
"completed": "[to be translated]:Completed", "completed": "Завершено",
"confirm": "Подтверждение", "confirm": "Подтверждение",
"copied": "Скопировано", "copied": "Скопировано",
"copy": "Копировать", "copy": "Копировать",
@ -3043,7 +3043,7 @@
"confirm": { "confirm": {
"button": "Выберите файл резервной копии" "button": "Выберите файл резервной копии"
}, },
"content": "[to be translated]:导出部分数据,包括聊天记录、设置。请注意,备份过程可能需要一些时间,感谢您的耐心等待。", "content": "Экспорт части данных, включая чат и настройки. Пожалуйста, обратите внимание, что процесс резервного копирования может занять некоторое время. Благодарим за ваше терпение.",
"lan": { "lan": {
"auto_close_tip": "Автоматическое закрытие через {{seconds}} секунд...", "auto_close_tip": "Автоматическое закрытие через {{seconds}} секунд...",
"confirm_close_message": "Передача файла в процессе. Закрытие прервет передачу. Вы уверены, что хотите принудительно закрыть?", "confirm_close_message": "Передача файла в процессе. Закрытие прервет передачу. Вы уверены, что хотите принудительно закрыть?",
@ -3077,7 +3077,7 @@
"title": "Передача по локальной сети", "title": "Передача по локальной сети",
"transfer_progress": "Прогресс передачи" "transfer_progress": "Прогресс передачи"
}, },
"title": "[to be translated]:导出至手机" "title": "Экспорт на телефон"
}, },
"hour_interval_one": "{{count}} час", "hour_interval_one": "{{count}} час",
"hour_interval_other": "{{count}} часов", "hour_interval_other": "{{count}} часов",

View File

@ -63,7 +63,7 @@ const PluginSettings: FC<PluginSettingsProps> = ({ agentBase }) => {
panel: 'w-full flex-1 overflow-hidden' panel: 'w-full flex-1 overflow-hidden'
}}> }}>
<Tab key="available" title={t('agent.settings.plugins.available.title')}> <Tab key="available" title={t('agent.settings.plugins.available.title')}>
<div className="flex h-full flex-col overflow-y-auto pt-4"> <div className="flex h-full flex-col overflow-y-auto pt-4 pr-2">
{errorAvailable ? ( {errorAvailable ? (
<Card className="bg-danger-50 dark:bg-danger-900/20"> <Card className="bg-danger-50 dark:bg-danger-900/20">
<CardBody> <CardBody>
@ -88,7 +88,7 @@ const PluginSettings: FC<PluginSettingsProps> = ({ agentBase }) => {
</Tab> </Tab>
<Tab key="installed" title={t('agent.settings.plugins.installed.title')}> <Tab key="installed" title={t('agent.settings.plugins.installed.title')}>
<div className="flex h-full flex-col overflow-y-auto pt-4"> <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> <CardBody>

View File

@ -1,53 +0,0 @@
import { Chip } from '@heroui/react'
import { FC } from 'react'
import { useTranslation } from 'react-i18next'
export interface CategoryFilterProps {
categories: string[]
selectedCategories: string[]
onChange: (categories: string[]) => void
}
export const CategoryFilter: FC<CategoryFilterProps> = ({ categories, selectedCategories, onChange }) => {
const { t } = useTranslation()
const isAllSelected = selectedCategories.length === 0
const handleCategoryClick = (category: string) => {
if (selectedCategories.includes(category)) {
onChange(selectedCategories.filter((c) => c !== category))
} else {
onChange([...selectedCategories, category])
}
}
const handleAllClick = () => {
onChange([])
}
return (
<div className="flex max-h-24 flex-wrap gap-2 overflow-y-auto">
<Chip
variant={isAllSelected ? 'solid' : 'bordered'}
color={isAllSelected ? 'primary' : 'default'}
onClick={handleAllClick}
className="cursor-pointer">
{t('plugins.all_categories')}
</Chip>
{categories.map((category) => {
const isSelected = selectedCategories.includes(category)
return (
<Chip
key={category}
variant={isSelected ? 'solid' : 'bordered'}
color={isSelected ? 'primary' : 'default'}
onClick={() => handleCategoryClick(category)}
className="cursor-pointer">
{category}
</Chip>
)
})}
</div>
)
}

View File

@ -1,10 +1,19 @@
import { Input, Pagination, Tab, Tabs } from '@heroui/react' import {
Button,
Dropdown,
DropdownItem,
DropdownMenu,
DropdownTrigger,
Input,
Pagination,
Tab,
Tabs
} from '@heroui/react'
import { InstalledPlugin, PluginMetadata } from '@renderer/types/plugin' import { InstalledPlugin, PluginMetadata } from '@renderer/types/plugin'
import { Search } from 'lucide-react' import { Filter, Search } from 'lucide-react'
import { FC, useMemo, useState } from 'react' import { FC, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { CategoryFilter } from './CategoryFilter'
import { PluginCard } from './PluginCard' import { PluginCard } from './PluginCard'
import { PluginDetailModal } from './PluginDetailModal' import { PluginDetailModal } from './PluginDetailModal'
@ -122,8 +131,13 @@ export const PluginBrowser: FC<PluginBrowserProps> = ({
setCurrentPage(1) setCurrentPage(1)
} }
const handleCategoryChange = (categories: string[]) => { const handleCategoryChange = (keys: Set<string>) => {
setSelectedCategories(categories) // Reset if "all" selected, otherwise filter categories
if (keys.has('all') || keys.size === 0) {
setSelectedCategories([])
} else {
setSelectedCategories(Array.from(keys).filter((key) => key !== 'all'))
}
setCurrentPage(1) setCurrentPage(1)
} }
@ -144,25 +158,71 @@ export const PluginBrowser: FC<PluginBrowserProps> = ({
return ( return (
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
{/* Search Input */} {/* Search and Filter */}
<Input <div className="flex gap-2">
placeholder={t('plugins.search_placeholder')} <Input
value={searchQuery} placeholder={t('plugins.search_placeholder')}
onValueChange={handleSearchChange} value={searchQuery}
startContent={<Search className="h-4 w-4 text-default-400" />} onValueChange={handleSearchChange}
isClearable startContent={<Search className="h-4 w-4 text-default-400" />}
classNames={{ isClearable
input: 'text-small', classNames={{
inputWrapper: 'h-10' input: 'text-small',
}} inputWrapper: 'h-10'
/> }}
className="flex-1"
/>
{/* Category Filter */} <Dropdown
<CategoryFilter placement="bottom-start"
categories={allCategories} classNames={{
selectedCategories={selectedCategories} content: 'max-h-60 overflow-y-auto p-0'
onChange={handleCategoryChange} }}>
/> <DropdownTrigger>
<Button
isIconOnly
variant={selectedCategories.length > 0 ? 'solid' : 'bordered'}
color={selectedCategories.length > 0 ? 'primary' : 'default'}
size="md"
className="h-10 min-w-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)
}
}}
className={isSelected ? 'bg-primary-50' : ''}>
{item.label}
{isSelected && <span className="ml-2 text-primary text-sm"></span>}
</DropdownItem>
)
}}
</DropdownMenu>
</Dropdown>
</div>
{/* Type Tabs */} {/* Type Tabs */}
<Tabs selectedKey={activeType} onSelectionChange={handleTypeChange} variant="underlined"> <Tabs selectedKey={activeType} onSelectionChange={handleTypeChange} variant="underlined">
@ -184,21 +244,22 @@ export const PluginBrowser: FC<PluginBrowserProps> = ({
<p className="text-default-300 text-small">{t('plugins.try_different_search')}</p> <p className="text-default-300 text-small">{t('plugins.try_different_search')}</p>
</div> </div>
) : ( ) : (
<div className="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3"> <div className="grid grid-cols-1 gap-4 md:grid-cols-2">
{paginatedPlugins.map((plugin) => { {paginatedPlugins.map((plugin) => {
const installed = isPluginInstalled(plugin) const installed = isPluginInstalled(plugin)
const isActioning = actioningPlugin === plugin.sourcePath const isActioning = actioningPlugin === plugin.sourcePath
return ( return (
<PluginCard <div key={`${plugin.type}-${plugin.sourcePath}`} className="h-full">
key={`${plugin.type}-${plugin.sourcePath}`} <PluginCard
plugin={plugin} plugin={plugin}
installed={installed} installed={installed}
onInstall={() => handleInstall(plugin)} onInstall={() => handleInstall(plugin)}
onUninstall={() => handleUninstall(plugin)} onUninstall={() => handleUninstall(plugin)}
loading={loading || isActioning} loading={loading || isActioning}
onClick={() => handlePluginClick(plugin)} onClick={() => handlePluginClick(plugin)}
/> />
</div>
) )
})} })}
</div> </div>

View File

@ -17,14 +17,19 @@ export const PluginCard: FC<PluginCardProps> = ({ plugin, installed, onInstall,
const { t } = useTranslation() const { t } = useTranslation()
return ( return (
<Card className="w-full cursor-pointer transition-shadow hover:shadow-md" isPressable onPress={onClick}> <Card
className="flex h-full w-full cursor-pointer flex-col transition-shadow hover:shadow-md"
isPressable
onPress={onClick}>
<CardHeader className="flex flex-col items-start gap-2 pb-2"> <CardHeader className="flex flex-col items-start gap-2 pb-2">
<div className="flex w-full items-center justify-between"> <div className="flex w-full items-center justify-between gap-2">
<h3 className="font-semibold text-medium">{plugin.name}</h3> <h3 className="truncate font-medium text-small">{plugin.name}</h3>
<Chip <Chip
size="sm" size="sm"
variant="solid" variant="solid"
color={plugin.type === 'agent' ? 'primary' : plugin.type === 'skill' ? 'success' : 'secondary'}> color={plugin.type === 'agent' ? 'primary' : plugin.type === 'skill' ? 'success' : 'secondary'}
className="h-4 min-w-0 flex-shrink-0 px-0.5"
style={{ fontSize: '10px' }}>
{plugin.type} {plugin.type}
</Chip> </Chip>
</div> </div>
@ -33,7 +38,7 @@ export const PluginCard: FC<PluginCardProps> = ({ plugin, installed, onInstall,
</Chip> </Chip>
</CardHeader> </CardHeader>
<CardBody className="py-2"> <CardBody className="flex-1 py-2">
<p className="line-clamp-3 text-default-500 text-small">{plugin.description || t('plugins.no_description')}</p> <p className="line-clamp-3 text-default-500 text-small">{plugin.description || t('plugins.no_description')}</p>
{plugin.tags && plugin.tags.length > 0 && ( {plugin.tags && plugin.tags.length > 0 && (

View File

@ -1,5 +1,3 @@
export type { CategoryFilterProps } from './CategoryFilter'
export { CategoryFilter } from './CategoryFilter'
export type { InstalledPluginsListProps } from './InstalledPluginsList' export type { InstalledPluginsListProps } from './InstalledPluginsList'
export { InstalledPluginsList } from './InstalledPluginsList' export { InstalledPluginsList } from './InstalledPluginsList'
export type { PluginBrowserProps } from './PluginBrowser' export type { PluginBrowserProps } from './PluginBrowser'