mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2026-01-02 10:29:02 +08:00
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:
parent
1018ad87b8
commit
562fbb3ff7
@ -3052,10 +3052,10 @@
|
||||
"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.",
|
||||
"error": {
|
||||
"init_failed": "[to be translated]:Initialization failed",
|
||||
"init_failed": "Falha na inicialização",
|
||||
"no_file": "Nenhum arquivo selecionado",
|
||||
"no_ip": "[to be translated]:Unable to get IP address",
|
||||
"send_failed": "[to be translated]:Failed to send file"
|
||||
"no_ip": "Incapaz de obter endereço IP",
|
||||
"send_failed": "Falha ao enviar arquivo"
|
||||
},
|
||||
"force_close": "Forçar Fechamento",
|
||||
"generating_qr": "Gerando código QR...",
|
||||
@ -3064,15 +3064,15 @@
|
||||
"selectZip": "Selecionar arquivo compactado",
|
||||
"sendZip": "Iniciar recuperação de dados",
|
||||
"status": {
|
||||
"completed": "[to be translated]:Transfer completed",
|
||||
"connected": "[to be translated]:Connected",
|
||||
"connecting": "[to be translated]:Connecting...",
|
||||
"disconnected": "[to be translated]:Disconnected",
|
||||
"error": "[to be translated]:Connection error",
|
||||
"initializing": "[to be translated]:Initializing connection...",
|
||||
"preparing": "[to be translated]:Preparing transfer...",
|
||||
"sending": "[to be translated]:Transferring {{progress}}%",
|
||||
"waiting_qr_scan": "[to be translated]:Please scan QR code to connect"
|
||||
"completed": "Transferência concluída",
|
||||
"connected": "Conectado",
|
||||
"connecting": "Conectando...",
|
||||
"disconnected": "Desconectado",
|
||||
"error": "Erro de conexão",
|
||||
"initializing": "Inicializando conexão...",
|
||||
"preparing": "Preparando transferência...",
|
||||
"sending": "Transferindo {{progress}}%",
|
||||
"waiting_qr_scan": "Por favor, escaneie o código QR para conectar"
|
||||
},
|
||||
"title": "transmissão de rede local",
|
||||
"transfer_progress": "Progresso da transferência"
|
||||
|
||||
@ -1047,7 +1047,7 @@
|
||||
"clear": "Очистить",
|
||||
"close": "Закрыть",
|
||||
"collapse": "Свернуть",
|
||||
"completed": "[to be translated]:Completed",
|
||||
"completed": "Завершено",
|
||||
"confirm": "Подтверждение",
|
||||
"copied": "Скопировано",
|
||||
"copy": "Копировать",
|
||||
@ -3043,7 +3043,7 @@
|
||||
"confirm": {
|
||||
"button": "Выберите файл резервной копии"
|
||||
},
|
||||
"content": "[to be translated]:导出部分数据,包括聊天记录、设置。请注意,备份过程可能需要一些时间,感谢您的耐心等待。",
|
||||
"content": "Экспорт части данных, включая чат и настройки. Пожалуйста, обратите внимание, что процесс резервного копирования может занять некоторое время. Благодарим за ваше терпение.",
|
||||
"lan": {
|
||||
"auto_close_tip": "Автоматическое закрытие через {{seconds}} секунд...",
|
||||
"confirm_close_message": "Передача файла в процессе. Закрытие прервет передачу. Вы уверены, что хотите принудительно закрыть?",
|
||||
@ -3077,7 +3077,7 @@
|
||||
"title": "Передача по локальной сети",
|
||||
"transfer_progress": "Прогресс передачи"
|
||||
},
|
||||
"title": "[to be translated]:导出至手机"
|
||||
"title": "Экспорт на телефон"
|
||||
},
|
||||
"hour_interval_one": "{{count}} час",
|
||||
"hour_interval_other": "{{count}} часов",
|
||||
|
||||
@ -63,7 +63,7 @@ const PluginSettings: FC<PluginSettingsProps> = ({ agentBase }) => {
|
||||
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-4">
|
||||
<div className="flex h-full flex-col overflow-y-auto pt-4 pr-2">
|
||||
{errorAvailable ? (
|
||||
<Card className="bg-danger-50 dark:bg-danger-900/20">
|
||||
<CardBody>
|
||||
@ -88,7 +88,7 @@ const PluginSettings: FC<PluginSettingsProps> = ({ agentBase }) => {
|
||||
</Tab>
|
||||
|
||||
<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 ? (
|
||||
<Card className="bg-danger-50 dark:bg-danger-900/20">
|
||||
<CardBody>
|
||||
|
||||
@ -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>
|
||||
)
|
||||
}
|
||||
@ -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 { Search } from 'lucide-react'
|
||||
import { Filter, Search } from 'lucide-react'
|
||||
import { FC, useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import { CategoryFilter } from './CategoryFilter'
|
||||
import { PluginCard } from './PluginCard'
|
||||
import { PluginDetailModal } from './PluginDetailModal'
|
||||
|
||||
@ -122,8 +131,13 @@ export const PluginBrowser: FC<PluginBrowserProps> = ({
|
||||
setCurrentPage(1)
|
||||
}
|
||||
|
||||
const handleCategoryChange = (categories: string[]) => {
|
||||
setSelectedCategories(categories)
|
||||
const handleCategoryChange = (keys: Set<string>) => {
|
||||
// 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)
|
||||
}
|
||||
|
||||
@ -144,25 +158,71 @@ export const PluginBrowser: FC<PluginBrowserProps> = ({
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
{/* Search Input */}
|
||||
<Input
|
||||
placeholder={t('plugins.search_placeholder')}
|
||||
value={searchQuery}
|
||||
onValueChange={handleSearchChange}
|
||||
startContent={<Search className="h-4 w-4 text-default-400" />}
|
||||
isClearable
|
||||
classNames={{
|
||||
input: 'text-small',
|
||||
inputWrapper: 'h-10'
|
||||
}}
|
||||
/>
|
||||
{/* Search and Filter */}
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
placeholder={t('plugins.search_placeholder')}
|
||||
value={searchQuery}
|
||||
onValueChange={handleSearchChange}
|
||||
startContent={<Search className="h-4 w-4 text-default-400" />}
|
||||
isClearable
|
||||
classNames={{
|
||||
input: 'text-small',
|
||||
inputWrapper: 'h-10'
|
||||
}}
|
||||
className="flex-1"
|
||||
/>
|
||||
|
||||
{/* Category Filter */}
|
||||
<CategoryFilter
|
||||
categories={allCategories}
|
||||
selectedCategories={selectedCategories}
|
||||
onChange={handleCategoryChange}
|
||||
/>
|
||||
<Dropdown
|
||||
placement="bottom-start"
|
||||
classNames={{
|
||||
content: 'max-h-60 overflow-y-auto p-0'
|
||||
}}>
|
||||
<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 */}
|
||||
<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>
|
||||
</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) => {
|
||||
const installed = isPluginInstalled(plugin)
|
||||
const isActioning = actioningPlugin === plugin.sourcePath
|
||||
|
||||
return (
|
||||
<PluginCard
|
||||
key={`${plugin.type}-${plugin.sourcePath}`}
|
||||
plugin={plugin}
|
||||
installed={installed}
|
||||
onInstall={() => handleInstall(plugin)}
|
||||
onUninstall={() => handleUninstall(plugin)}
|
||||
loading={loading || isActioning}
|
||||
onClick={() => handlePluginClick(plugin)}
|
||||
/>
|
||||
<div key={`${plugin.type}-${plugin.sourcePath}`} className="h-full">
|
||||
<PluginCard
|
||||
plugin={plugin}
|
||||
installed={installed}
|
||||
onInstall={() => handleInstall(plugin)}
|
||||
onUninstall={() => handleUninstall(plugin)}
|
||||
loading={loading || isActioning}
|
||||
onClick={() => handlePluginClick(plugin)}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
@ -17,14 +17,19 @@ export const PluginCard: FC<PluginCardProps> = ({ plugin, installed, onInstall,
|
||||
const { t } = useTranslation()
|
||||
|
||||
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">
|
||||
<div className="flex w-full items-center justify-between">
|
||||
<h3 className="font-semibold text-medium">{plugin.name}</h3>
|
||||
<div className="flex w-full items-center justify-between gap-2">
|
||||
<h3 className="truncate font-medium text-small">{plugin.name}</h3>
|
||||
<Chip
|
||||
size="sm"
|
||||
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}
|
||||
</Chip>
|
||||
</div>
|
||||
@ -33,7 +38,7 @@ export const PluginCard: FC<PluginCardProps> = ({ plugin, installed, onInstall,
|
||||
</Chip>
|
||||
</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>
|
||||
|
||||
{plugin.tags && plugin.tags.length > 0 && (
|
||||
|
||||
@ -1,5 +1,3 @@
|
||||
export type { CategoryFilterProps } from './CategoryFilter'
|
||||
export { CategoryFilter } from './CategoryFilter'
|
||||
export type { InstalledPluginsListProps } from './InstalledPluginsList'
|
||||
export { InstalledPluginsList } from './InstalledPluginsList'
|
||||
export type { PluginBrowserProps } from './PluginBrowser'
|
||||
|
||||
Loading…
Reference in New Issue
Block a user