mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2026-01-03 02:59:07 +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",
|
"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"
|
||||||
|
|||||||
@ -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}} часов",
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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 { 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>
|
||||||
|
|||||||
@ -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 && (
|
||||||
|
|||||||
@ -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'
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user