mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2025-12-19 22:52:08 +08:00
feat: add AddAssistantOrAgentPopup component and update i18n translations
- Introduced a new AddAssistantOrAgentPopup component for selecting between assistant and agent options. - Updated English, Simplified Chinese, and Traditional Chinese translations to include descriptions and titles for assistant and agent options. - Refactored UnifiedAddButton to utilize the new popup for adding assistants or agents.
This commit is contained in:
parent
562fbb3ff7
commit
fb02a61a48
@ -23,7 +23,7 @@
|
||||
},
|
||||
"files": {
|
||||
"ignoreUnknown": false,
|
||||
"includes": ["**"],
|
||||
"includes": ["**", "!**/.claude/**"],
|
||||
"maxSize": 2097152
|
||||
},
|
||||
"formatter": {
|
||||
|
||||
119
src/renderer/src/components/Popups/AddAssistantOrAgentPopup.tsx
Normal file
119
src/renderer/src/components/Popups/AddAssistantOrAgentPopup.tsx
Normal file
@ -0,0 +1,119 @@
|
||||
import { cn } from '@heroui/react'
|
||||
import { TopView } from '@renderer/components/TopView'
|
||||
import { Modal } from 'antd'
|
||||
import { Bot, MessageSquare } from 'lucide-react'
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
type OptionType = 'assistant' | 'agent'
|
||||
|
||||
interface ShowParams {
|
||||
onSelect: (type: OptionType) => void
|
||||
}
|
||||
|
||||
interface Props extends ShowParams {
|
||||
resolve: (data: { type?: OptionType }) => void
|
||||
}
|
||||
|
||||
const PopupContainer: React.FC<Props> = ({ onSelect, resolve }) => {
|
||||
const { t } = useTranslation()
|
||||
const [open, setOpen] = useState(true)
|
||||
const [hoveredOption, setHoveredOption] = useState<OptionType | null>(null)
|
||||
|
||||
const onCancel = () => {
|
||||
setOpen(false)
|
||||
}
|
||||
|
||||
const onClose = () => {
|
||||
resolve({})
|
||||
}
|
||||
|
||||
const handleSelect = (type: OptionType) => {
|
||||
setOpen(false)
|
||||
onSelect(type)
|
||||
resolve({ type })
|
||||
}
|
||||
|
||||
AddAssistantOrAgentPopup.hide = onCancel
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={t('chat.add.option.title')}
|
||||
open={open}
|
||||
onCancel={onCancel}
|
||||
afterClose={onClose}
|
||||
transitionName="animation-move-down"
|
||||
centered
|
||||
footer={null}
|
||||
width={560}>
|
||||
<div className="grid grid-cols-2 gap-4 py-4">
|
||||
{/* Assistant Option */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleSelect('assistant')}
|
||||
className="group flex flex-col items-center gap-3 rounded-lg bg-[var(--color-background-soft)] p-6 transition-all hover:bg-[var(--color-hover)]"
|
||||
onMouseEnter={() => setHoveredOption('assistant')}
|
||||
onMouseLeave={() => setHoveredOption(null)}>
|
||||
<div className="flex h-12 w-12 items-center justify-center rounded-full bg-[var(--color-list-item)] transition-colors">
|
||||
<MessageSquare
|
||||
size={24}
|
||||
className={cn(
|
||||
'transition-colors',
|
||||
hoveredOption === 'assistant' ? 'text-[var(--color-primary)]' : 'text-[var(--color-icon-white)]'
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<h3 className="mb-1 font-semibold text-[var(--color-text-1)] text-base">{t('chat.add.assistant.title')}</h3>
|
||||
<p className="text-[var(--color-text-2)] text-sm">{t('chat.add.assistant.description')}</p>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
{/* Agent Option */}
|
||||
<button
|
||||
onClick={() => handleSelect('agent')}
|
||||
type="button"
|
||||
className="group flex flex-col items-center gap-3 rounded-lg bg-[var(--color-background-soft)] p-6 transition-all hover:bg-[var(--color-hover)]"
|
||||
onMouseEnter={() => setHoveredOption('agent')}
|
||||
onMouseLeave={() => setHoveredOption(null)}>
|
||||
<div className="flex h-12 w-12 items-center justify-center rounded-full bg-[var(--color-list-item)] transition-colors">
|
||||
<Bot
|
||||
size={24}
|
||||
className={cn(
|
||||
'transition-colors',
|
||||
hoveredOption === 'agent' ? 'text-[var(--color-primary)]' : 'text-[var(--color-icon-white)]'
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<h3 className="mb-1 font-semibold text-[var(--color-text-1)] text-base">{t('agent.add.title')}</h3>
|
||||
<p className="text-[var(--color-text-2)] text-sm">{t('agent.add.description')}</p>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
const TopViewKey = 'AddAssistantOrAgentPopup'
|
||||
|
||||
export default class AddAssistantOrAgentPopup {
|
||||
static topviewId = 0
|
||||
static hide() {
|
||||
TopView.hide(TopViewKey)
|
||||
}
|
||||
static show(props: ShowParams) {
|
||||
return new Promise<{ type?: OptionType }>((resolve) => {
|
||||
TopView.show(
|
||||
<PopupContainer
|
||||
{...props}
|
||||
resolve={(v) => {
|
||||
resolve(v)
|
||||
TopView.hide(TopViewKey)
|
||||
}}
|
||||
/>,
|
||||
TopViewKey
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -1,6 +1,7 @@
|
||||
{
|
||||
"agent": {
|
||||
"add": {
|
||||
"description": "Handle complex tasks with various tools",
|
||||
"error": {
|
||||
"failed": "Failed to add a agent",
|
||||
"invalid_agent": "Invalid Agent"
|
||||
@ -547,8 +548,12 @@
|
||||
"chat": {
|
||||
"add": {
|
||||
"assistant": {
|
||||
"description": "Daily conversations and quick Q&A",
|
||||
"title": "Add Assistant"
|
||||
},
|
||||
"option": {
|
||||
"title": "Select Type"
|
||||
},
|
||||
"topic": {
|
||||
"title": "New Topic"
|
||||
}
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
{
|
||||
"agent": {
|
||||
"add": {
|
||||
"description": "调用各种工具处理复杂任务",
|
||||
"error": {
|
||||
"failed": "添加 Agent 失败",
|
||||
"invalid_agent": "无效的 Agent"
|
||||
@ -547,8 +548,12 @@
|
||||
"chat": {
|
||||
"add": {
|
||||
"assistant": {
|
||||
"description": "日常对话和快速问答",
|
||||
"title": "添加助手"
|
||||
},
|
||||
"option": {
|
||||
"title": "选择添加类型"
|
||||
},
|
||||
"topic": {
|
||||
"title": "新建话题"
|
||||
}
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
{
|
||||
"agent": {
|
||||
"add": {
|
||||
"description": "調用各種工具處理複雜任務",
|
||||
"error": {
|
||||
"failed": "無法新增代理人",
|
||||
"invalid_agent": "無效的 Agent"
|
||||
@ -547,8 +548,12 @@
|
||||
"chat": {
|
||||
"add": {
|
||||
"assistant": {
|
||||
"description": "日常對話和快速問答",
|
||||
"title": "新增助手"
|
||||
},
|
||||
"option": {
|
||||
"title": "選擇新增類型"
|
||||
},
|
||||
"topic": {
|
||||
"title": "新增話題"
|
||||
}
|
||||
|
||||
@ -170,7 +170,7 @@ const AssistantsTab: FC<AssistantsTabProps> = (props) => {
|
||||
onAssistantSwitch={setActiveAssistant}
|
||||
onAssistantDelete={onDeleteAssistant}
|
||||
onAgentDelete={deleteAgent}
|
||||
onAgentPress={setActiveAgentId}
|
||||
onAgentPress={handleAgentPress}
|
||||
addPreset={addAssistantPreset}
|
||||
copyAssistant={copyAssistant}
|
||||
onCreateDefaultAssistant={onCreateDefaultAssistant}
|
||||
|
||||
@ -84,7 +84,8 @@ export const Container: React.FC<{ isActive?: boolean } & React.HTMLAttributes<H
|
||||
}) => (
|
||||
<div
|
||||
className={cn(
|
||||
'relative flex h-[37px] w-[calc(var(--assistants-width)-20px)] cursor-pointer flex-row justify-between rounded-[var(--list-item-border-radius)] border border-transparent px-2 hover:bg-[var(--color-list-item-hover)]',
|
||||
'relative flex h-[37px] w-[calc(var(--assistants-width)-20px)] cursor-pointer flex-row justify-between rounded-[var(--list-item-border-radius)] border border-transparent px-2',
|
||||
!isActive && 'hover:bg-[var(--color-list-item-hover)]',
|
||||
isActive && 'bg-[var(--color-list-item)] shadow-[0_1px_2px_0_rgba(0,0,0,0.05)]',
|
||||
className
|
||||
)}
|
||||
|
||||
@ -398,7 +398,8 @@ const Container = ({
|
||||
<div
|
||||
{...props}
|
||||
className={cn(
|
||||
'relative flex h-[37px] w-[calc(var(--assistants-width)-20px)] cursor-pointer flex-row justify-between rounded-[var(--list-item-border-radius)] border-[0.5px] border-transparent px-2 hover:bg-[var(--color-list-item-hover)]',
|
||||
'relative flex h-[37px] w-[calc(var(--assistants-width)-20px)] cursor-pointer flex-row justify-between rounded-[var(--list-item-border-radius)] border-[0.5px] border-transparent px-2',
|
||||
!isActive && 'hover:bg-[var(--color-list-item-hover)]',
|
||||
isActive && 'bg-[var(--color-list-item)] shadow-[0_1px_2px_0_rgba(0,0,0,0.05)]',
|
||||
className
|
||||
)}>
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
import { Button, Popover, PopoverContent, PopoverTrigger, useDisclosure } from '@heroui/react'
|
||||
import { useDisclosure } from '@heroui/react'
|
||||
import AddAssistantOrAgentPopup from '@renderer/components/Popups/AddAssistantOrAgentPopup'
|
||||
import { AgentModal } from '@renderer/components/Popups/agent/AgentModal'
|
||||
import { useAppDispatch } from '@renderer/store'
|
||||
import { setActiveTopicOrSessionAction } from '@renderer/store/runtime'
|
||||
import { AgentEntity, Assistant, Topic } from '@renderer/types'
|
||||
import { Bot, MessageSquare } from 'lucide-react'
|
||||
import { FC, useCallback, useState } from 'react'
|
||||
import { FC, useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import AddButton from './AddButton'
|
||||
@ -17,19 +17,20 @@ interface UnifiedAddButtonProps {
|
||||
|
||||
const UnifiedAddButton: FC<UnifiedAddButtonProps> = ({ onCreateAssistant, setActiveAssistant, setActiveAgentId }) => {
|
||||
const { t } = useTranslation()
|
||||
const [isPopoverOpen, setIsPopoverOpen] = useState(false)
|
||||
const { isOpen: isAgentModalOpen, onOpen: onAgentModalOpen, onClose: onAgentModalClose } = useDisclosure()
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
const handleAddAssistant = () => {
|
||||
setIsPopoverOpen(false)
|
||||
const handleAddButtonClick = () => {
|
||||
AddAssistantOrAgentPopup.show({
|
||||
onSelect: (type) => {
|
||||
if (type === 'assistant') {
|
||||
onCreateAssistant()
|
||||
}
|
||||
|
||||
const handleAddAgent = () => {
|
||||
setIsPopoverOpen(false)
|
||||
} else if (type === 'agent') {
|
||||
onAgentModalOpen()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const afterCreate = useCallback(
|
||||
(a: AgentEntity) => {
|
||||
@ -58,32 +59,9 @@ const UnifiedAddButton: FC<UnifiedAddButtonProps> = ({ onCreateAssistant, setAct
|
||||
|
||||
return (
|
||||
<div className="mb-1">
|
||||
<Popover
|
||||
isOpen={isPopoverOpen}
|
||||
onOpenChange={setIsPopoverOpen}
|
||||
placement="bottom"
|
||||
classNames={{ content: 'p-0 min-w-[200px]' }}>
|
||||
<PopoverTrigger>
|
||||
<AddButton>{t('chat.add.assistant.title')}</AddButton>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent>
|
||||
<div className="flex w-full flex-col gap-1 p-1">
|
||||
<Button
|
||||
onPress={handleAddAssistant}
|
||||
className="w-full justify-start bg-transparent hover:bg-[var(--color-list-item)]"
|
||||
startContent={<MessageSquare size={16} className="shrink-0" />}>
|
||||
<AddButton onPress={handleAddButtonClick} className="-mt-[1px] mb-[2px]">
|
||||
{t('chat.add.assistant.title')}
|
||||
</Button>
|
||||
<Button
|
||||
onPress={handleAddAgent}
|
||||
className="w-full justify-start bg-transparent hover:bg-[var(--color-list-item)]"
|
||||
startContent={<Bot size={16} className="shrink-0" />}>
|
||||
{t('agent.add.title')}
|
||||
</Button>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
|
||||
</AddButton>
|
||||
<AgentModal isOpen={isAgentModalOpen} onClose={onAgentModalClose} afterSubmit={afterCreate} />
|
||||
</div>
|
||||
)
|
||||
|
||||
@ -54,7 +54,7 @@ const PluginSettings: FC<PluginSettingsProps> = ({ agentBase }) => {
|
||||
)
|
||||
|
||||
return (
|
||||
<SettingsContainer>
|
||||
<SettingsContainer className="pr-0">
|
||||
<Tabs
|
||||
aria-label="Plugin settings tabs"
|
||||
classNames={{
|
||||
@ -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 pr-2">
|
||||
<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>
|
||||
|
||||
@ -168,6 +168,7 @@ export const ToolingSettings: FC<AgentToolingSettingsProps> = ({ agentBase, upda
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
centered: true,
|
||||
okText: t('common.confirm'),
|
||||
cancelText: t('common.cancel'),
|
||||
onOk: applyChange,
|
||||
@ -274,9 +275,10 @@ export const ToolingSettings: FC<AgentToolingSettingsProps> = ({ agentBase, upda
|
||||
key={card.mode}
|
||||
isPressable={!disabled}
|
||||
isDisabled={disabled || isUpdatingMode}
|
||||
shadow="none"
|
||||
onPress={() => handleSelectPermissionMode(card.mode)}
|
||||
className={`border ${
|
||||
isSelected ? 'border-primary shadow-lg' : 'border-default-200'
|
||||
isSelected ? 'border-primary' : 'border-default-200'
|
||||
} ${disabled ? 'opacity-60' : ''}`}>
|
||||
<CardHeader className="flex items-start justify-between gap-2">
|
||||
<div className="flex flex-col">
|
||||
|
||||
@ -55,7 +55,7 @@ export const InstalledPluginsList: FC<InstalledPluginsListProps> = ({ plugins, o
|
||||
<TableColumn>{t('plugins.name')}</TableColumn>
|
||||
<TableColumn>{t('plugins.type')}</TableColumn>
|
||||
<TableColumn>{t('plugins.category')}</TableColumn>
|
||||
<TableColumn width={100}>{t('plugins.actions')}</TableColumn>
|
||||
<TableColumn align="end">{t('plugins.actions')}</TableColumn>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{plugins.map((plugin) => (
|
||||
|
||||
@ -1,17 +1,7 @@
|
||||
import {
|
||||
Button,
|
||||
Dropdown,
|
||||
DropdownItem,
|
||||
DropdownMenu,
|
||||
DropdownTrigger,
|
||||
Input,
|
||||
Pagination,
|
||||
Tab,
|
||||
Tabs
|
||||
} from '@heroui/react'
|
||||
import { Button, Dropdown, DropdownItem, DropdownMenu, DropdownTrigger, Input, Tab, Tabs } from '@heroui/react'
|
||||
import { InstalledPlugin, PluginMetadata } from '@renderer/types/plugin'
|
||||
import { Filter, Search } from 'lucide-react'
|
||||
import { FC, useMemo, useState } from 'react'
|
||||
import { FC, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import { PluginCard } from './PluginCard'
|
||||
@ -46,10 +36,11 @@ export const PluginBrowser: FC<PluginBrowserProps> = ({
|
||||
const [searchQuery, setSearchQuery] = useState('')
|
||||
const [selectedCategories, setSelectedCategories] = useState<string[]>([])
|
||||
const [activeType, setActiveType] = useState<PluginType>('all')
|
||||
const [currentPage, setCurrentPage] = useState(1)
|
||||
const [displayCount, setDisplayCount] = useState(ITEMS_PER_PAGE)
|
||||
const [actioningPlugin, setActioningPlugin] = useState<string | null>(null)
|
||||
const [selectedPlugin, setSelectedPlugin] = useState<PluginMetadata | null>(null)
|
||||
const [isModalOpen, setIsModalOpen] = useState(false)
|
||||
const observerTarget = useRef<HTMLDivElement>(null)
|
||||
|
||||
// Combine all plugins based on active type
|
||||
const allPlugins = useMemo(() => {
|
||||
@ -95,14 +86,35 @@ export const PluginBrowser: FC<PluginBrowserProps> = ({
|
||||
})
|
||||
}, [allPlugins, searchQuery, selectedCategories])
|
||||
|
||||
// Paginate filtered plugins
|
||||
const paginatedPlugins = useMemo(() => {
|
||||
const startIndex = (currentPage - 1) * ITEMS_PER_PAGE
|
||||
const endIndex = startIndex + ITEMS_PER_PAGE
|
||||
return filteredPlugins.slice(startIndex, endIndex)
|
||||
}, [filteredPlugins, currentPage])
|
||||
// Display plugins based on displayCount
|
||||
const displayedPlugins = useMemo(() => {
|
||||
return filteredPlugins.slice(0, displayCount)
|
||||
}, [filteredPlugins, displayCount])
|
||||
|
||||
const totalPages = Math.ceil(filteredPlugins.length / ITEMS_PER_PAGE)
|
||||
const hasMore = displayCount < filteredPlugins.length
|
||||
|
||||
// Reset display count when filters change
|
||||
useEffect(() => {
|
||||
setDisplayCount(ITEMS_PER_PAGE)
|
||||
}, [filteredPlugins])
|
||||
|
||||
// Infinite scroll observer
|
||||
useEffect(() => {
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
if (entries[0].isIntersecting && hasMore) {
|
||||
setDisplayCount((prev) => prev + ITEMS_PER_PAGE)
|
||||
}
|
||||
},
|
||||
{ threshold: 0.1 }
|
||||
)
|
||||
|
||||
if (observerTarget.current) {
|
||||
observer.observe(observerTarget.current)
|
||||
}
|
||||
|
||||
return () => observer.disconnect()
|
||||
}, [hasMore])
|
||||
|
||||
// Check if a plugin is installed
|
||||
const isPluginInstalled = (plugin: PluginMetadata): boolean => {
|
||||
@ -125,10 +137,9 @@ export const PluginBrowser: FC<PluginBrowserProps> = ({
|
||||
setActioningPlugin(null)
|
||||
}
|
||||
|
||||
// Reset to first page when filters change
|
||||
// Reset display count when filters change
|
||||
const handleSearchChange = (value: string) => {
|
||||
setSearchQuery(value)
|
||||
setCurrentPage(1)
|
||||
}
|
||||
|
||||
const handleCategoryChange = (keys: Set<string>) => {
|
||||
@ -138,12 +149,10 @@ export const PluginBrowser: FC<PluginBrowserProps> = ({
|
||||
} else {
|
||||
setSelectedCategories(Array.from(keys).filter((key) => key !== 'all'))
|
||||
}
|
||||
setCurrentPage(1)
|
||||
}
|
||||
|
||||
const handleTypeChange = (type: string | number) => {
|
||||
setActiveType(type as PluginType)
|
||||
setCurrentPage(1)
|
||||
}
|
||||
|
||||
const handlePluginClick = (plugin: PluginMetadata) => {
|
||||
@ -159,32 +168,27 @@ export const PluginBrowser: FC<PluginBrowserProps> = ({
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
{/* Search and Filter */}
|
||||
<div className="flex gap-2">
|
||||
<div className="relative flex gap-0">
|
||||
<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'
|
||||
}}
|
||||
size="md"
|
||||
className="flex-1"
|
||||
/>
|
||||
|
||||
<Dropdown
|
||||
placement="bottom-start"
|
||||
classNames={{
|
||||
content: 'max-h-60 overflow-y-auto p-0'
|
||||
}}>
|
||||
inputWrapper: 'pr-12'
|
||||
}}
|
||||
/>
|
||||
<Dropdown placement="bottom-end" classNames={{ content: 'max-h-60 overflow-y-auto p-0' }}>
|
||||
<DropdownTrigger>
|
||||
<Button
|
||||
isIconOnly
|
||||
variant={selectedCategories.length > 0 ? 'solid' : 'bordered'}
|
||||
variant={selectedCategories.length > 0 ? 'flat' : 'light'}
|
||||
color={selectedCategories.length > 0 ? 'primary' : 'default'}
|
||||
size="md"
|
||||
className="h-10 min-w-10">
|
||||
size="sm"
|
||||
className="-translate-y-1/2 absolute top-1/2 right-2 z-10">
|
||||
<Filter className="h-4 w-4" />
|
||||
</Button>
|
||||
</DropdownTrigger>
|
||||
@ -225,12 +229,14 @@ export const PluginBrowser: FC<PluginBrowserProps> = ({
|
||||
</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>
|
||||
|
||||
{/* Result Count */}
|
||||
<div className="flex items-center justify-between">
|
||||
@ -238,14 +244,15 @@ export const PluginBrowser: FC<PluginBrowserProps> = ({
|
||||
</div>
|
||||
|
||||
{/* Plugin Grid */}
|
||||
{paginatedPlugins.length === 0 ? (
|
||||
{displayedPlugins.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center py-12 text-center">
|
||||
<p className="text-default-400">{t('plugins.no_results')}</p>
|
||||
<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">
|
||||
{paginatedPlugins.map((plugin) => {
|
||||
{displayedPlugins.map((plugin) => {
|
||||
const installed = isPluginInstalled(plugin)
|
||||
const isActioning = actioningPlugin === plugin.sourcePath
|
||||
|
||||
@ -263,13 +270,9 @@ export const PluginBrowser: FC<PluginBrowserProps> = ({
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Pagination */}
|
||||
{totalPages > 1 && (
|
||||
<div className="flex justify-center">
|
||||
<Pagination total={totalPages} page={currentPage} onChange={setCurrentPage} showControls />
|
||||
</div>
|
||||
{/* Infinite scroll trigger */}
|
||||
{hasMore && <div ref={observerTarget} className="h-10" />}
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Plugin Detail Modal */}
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { Button, Card, CardBody, CardFooter, CardHeader, Chip, Spinner } from '@heroui/react'
|
||||
import { PluginMetadata } from '@renderer/types/plugin'
|
||||
import { upperFirst } from 'lodash'
|
||||
import { Download, Trash2 } from 'lucide-react'
|
||||
import { FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
@ -18,8 +19,9 @@ export const PluginCard: FC<PluginCardProps> = ({ plugin, installed, onInstall,
|
||||
|
||||
return (
|
||||
<Card
|
||||
className="flex h-full w-full cursor-pointer flex-col transition-shadow hover:shadow-md"
|
||||
className="flex h-full w-full cursor-pointer flex-col border-[0.5px] border-default-200"
|
||||
isPressable
|
||||
shadow="none"
|
||||
onPress={onClick}>
|
||||
<CardHeader className="flex flex-col items-start gap-2 pb-2">
|
||||
<div className="flex w-full items-center justify-between gap-2">
|
||||
@ -28,9 +30,8 @@ export const PluginCard: FC<PluginCardProps> = ({ plugin, installed, onInstall,
|
||||
size="sm"
|
||||
variant="solid"
|
||||
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}
|
||||
className="h-4 min-w-0 flex-shrink-0 px-0.5 text-xs">
|
||||
{upperFirst(plugin.type)}
|
||||
</Chip>
|
||||
</div>
|
||||
<Chip size="sm" variant="dot" color="default">
|
||||
|
||||
@ -9,7 +9,8 @@
|
||||
"packages/shared/**/*",
|
||||
"scripts",
|
||||
"packages/mcp-trace/**/*",
|
||||
"src/renderer/src/services/traceApi.ts" ],
|
||||
"src/renderer/src/services/traceApi.ts"
|
||||
],
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"incremental": true,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user