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:
kangfenmao 2025-10-31 22:26:09 +08:00
parent 562fbb3ff7
commit fb02a61a48
15 changed files with 242 additions and 121 deletions

View File

@ -23,7 +23,7 @@
}, },
"files": { "files": {
"ignoreUnknown": false, "ignoreUnknown": false,
"includes": ["**"], "includes": ["**", "!**/.claude/**"],
"maxSize": 2097152 "maxSize": 2097152
}, },
"formatter": { "formatter": {

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

View File

@ -1,6 +1,7 @@
{ {
"agent": { "agent": {
"add": { "add": {
"description": "Handle complex tasks with various tools",
"error": { "error": {
"failed": "Failed to add a agent", "failed": "Failed to add a agent",
"invalid_agent": "Invalid Agent" "invalid_agent": "Invalid Agent"
@ -547,8 +548,12 @@
"chat": { "chat": {
"add": { "add": {
"assistant": { "assistant": {
"description": "Daily conversations and quick Q&A",
"title": "Add Assistant" "title": "Add Assistant"
}, },
"option": {
"title": "Select Type"
},
"topic": { "topic": {
"title": "New Topic" "title": "New Topic"
} }

View File

@ -1,6 +1,7 @@
{ {
"agent": { "agent": {
"add": { "add": {
"description": "调用各种工具处理复杂任务",
"error": { "error": {
"failed": "添加 Agent 失败", "failed": "添加 Agent 失败",
"invalid_agent": "无效的 Agent" "invalid_agent": "无效的 Agent"
@ -547,8 +548,12 @@
"chat": { "chat": {
"add": { "add": {
"assistant": { "assistant": {
"description": "日常对话和快速问答",
"title": "添加助手" "title": "添加助手"
}, },
"option": {
"title": "选择添加类型"
},
"topic": { "topic": {
"title": "新建话题" "title": "新建话题"
} }

View File

@ -1,6 +1,7 @@
{ {
"agent": { "agent": {
"add": { "add": {
"description": "調用各種工具處理複雜任務",
"error": { "error": {
"failed": "無法新增代理人", "failed": "無法新增代理人",
"invalid_agent": "無效的 Agent" "invalid_agent": "無效的 Agent"
@ -547,8 +548,12 @@
"chat": { "chat": {
"add": { "add": {
"assistant": { "assistant": {
"description": "日常對話和快速問答",
"title": "新增助手" "title": "新增助手"
}, },
"option": {
"title": "選擇新增類型"
},
"topic": { "topic": {
"title": "新增話題" "title": "新增話題"
} }

View File

@ -170,7 +170,7 @@ const AssistantsTab: FC<AssistantsTabProps> = (props) => {
onAssistantSwitch={setActiveAssistant} onAssistantSwitch={setActiveAssistant}
onAssistantDelete={onDeleteAssistant} onAssistantDelete={onDeleteAssistant}
onAgentDelete={deleteAgent} onAgentDelete={deleteAgent}
onAgentPress={setActiveAgentId} onAgentPress={handleAgentPress}
addPreset={addAssistantPreset} addPreset={addAssistantPreset}
copyAssistant={copyAssistant} copyAssistant={copyAssistant}
onCreateDefaultAssistant={onCreateDefaultAssistant} onCreateDefaultAssistant={onCreateDefaultAssistant}

View File

@ -84,7 +84,8 @@ export const Container: React.FC<{ isActive?: boolean } & React.HTMLAttributes<H
}) => ( }) => (
<div <div
className={cn( 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)]', isActive && 'bg-[var(--color-list-item)] shadow-[0_1px_2px_0_rgba(0,0,0,0.05)]',
className className
)} )}

View File

@ -398,7 +398,8 @@ const Container = ({
<div <div
{...props} {...props}
className={cn( 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)]', isActive && 'bg-[var(--color-list-item)] shadow-[0_1px_2px_0_rgba(0,0,0,0.05)]',
className className
)}> )}>

View File

@ -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 { AgentModal } from '@renderer/components/Popups/agent/AgentModal'
import { useAppDispatch } from '@renderer/store' import { useAppDispatch } from '@renderer/store'
import { setActiveTopicOrSessionAction } from '@renderer/store/runtime' import { setActiveTopicOrSessionAction } from '@renderer/store/runtime'
import { AgentEntity, Assistant, Topic } from '@renderer/types' import { AgentEntity, Assistant, Topic } from '@renderer/types'
import { Bot, MessageSquare } from 'lucide-react' import { FC, useCallback } from 'react'
import { FC, useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import AddButton from './AddButton' import AddButton from './AddButton'
@ -17,18 +17,19 @@ interface UnifiedAddButtonProps {
const UnifiedAddButton: FC<UnifiedAddButtonProps> = ({ onCreateAssistant, setActiveAssistant, setActiveAgentId }) => { const UnifiedAddButton: FC<UnifiedAddButtonProps> = ({ onCreateAssistant, setActiveAssistant, setActiveAgentId }) => {
const { t } = useTranslation() const { t } = useTranslation()
const [isPopoverOpen, setIsPopoverOpen] = useState(false)
const { isOpen: isAgentModalOpen, onOpen: onAgentModalOpen, onClose: onAgentModalClose } = useDisclosure() const { isOpen: isAgentModalOpen, onOpen: onAgentModalOpen, onClose: onAgentModalClose } = useDisclosure()
const dispatch = useAppDispatch() const dispatch = useAppDispatch()
const handleAddAssistant = () => { const handleAddButtonClick = () => {
setIsPopoverOpen(false) AddAssistantOrAgentPopup.show({
onCreateAssistant() onSelect: (type) => {
} if (type === 'assistant') {
onCreateAssistant()
const handleAddAgent = () => { } else if (type === 'agent') {
setIsPopoverOpen(false) onAgentModalOpen()
onAgentModalOpen() }
}
})
} }
const afterCreate = useCallback( const afterCreate = useCallback(
@ -58,32 +59,9 @@ const UnifiedAddButton: FC<UnifiedAddButtonProps> = ({ onCreateAssistant, setAct
return ( return (
<div className="mb-1"> <div className="mb-1">
<Popover <AddButton onPress={handleAddButtonClick} className="-mt-[1px] mb-[2px]">
isOpen={isPopoverOpen} {t('chat.add.assistant.title')}
onOpenChange={setIsPopoverOpen} </AddButton>
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" />}>
{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>
<AgentModal isOpen={isAgentModalOpen} onClose={onAgentModalClose} afterSubmit={afterCreate} /> <AgentModal isOpen={isAgentModalOpen} onClose={onAgentModalClose} afterSubmit={afterCreate} />
</div> </div>
) )

View File

@ -54,7 +54,7 @@ const PluginSettings: FC<PluginSettingsProps> = ({ agentBase }) => {
) )
return ( return (
<SettingsContainer> <SettingsContainer className="pr-0">
<Tabs <Tabs
aria-label="Plugin settings tabs" aria-label="Plugin settings tabs"
classNames={{ classNames={{
@ -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 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 className="bg-danger-50 dark:bg-danger-900/20">
<CardBody> <CardBody>

View File

@ -168,6 +168,7 @@ export const ToolingSettings: FC<AgentToolingSettingsProps> = ({ agentBase, upda
</div> </div>
</div> </div>
), ),
centered: true,
okText: t('common.confirm'), okText: t('common.confirm'),
cancelText: t('common.cancel'), cancelText: t('common.cancel'),
onOk: applyChange, onOk: applyChange,
@ -274,9 +275,10 @@ export const ToolingSettings: FC<AgentToolingSettingsProps> = ({ agentBase, upda
key={card.mode} key={card.mode}
isPressable={!disabled} isPressable={!disabled}
isDisabled={disabled || isUpdatingMode} isDisabled={disabled || isUpdatingMode}
shadow="none"
onPress={() => handleSelectPermissionMode(card.mode)} onPress={() => handleSelectPermissionMode(card.mode)}
className={`border ${ className={`border ${
isSelected ? 'border-primary shadow-lg' : 'border-default-200' isSelected ? 'border-primary' : 'border-default-200'
} ${disabled ? 'opacity-60' : ''}`}> } ${disabled ? 'opacity-60' : ''}`}>
<CardHeader className="flex items-start justify-between gap-2"> <CardHeader className="flex items-start justify-between gap-2">
<div className="flex flex-col"> <div className="flex flex-col">

View File

@ -55,7 +55,7 @@ export const InstalledPluginsList: FC<InstalledPluginsListProps> = ({ plugins, o
<TableColumn>{t('plugins.name')}</TableColumn> <TableColumn>{t('plugins.name')}</TableColumn>
<TableColumn>{t('plugins.type')}</TableColumn> <TableColumn>{t('plugins.type')}</TableColumn>
<TableColumn>{t('plugins.category')}</TableColumn> <TableColumn>{t('plugins.category')}</TableColumn>
<TableColumn width={100}>{t('plugins.actions')}</TableColumn> <TableColumn align="end">{t('plugins.actions')}</TableColumn>
</TableHeader> </TableHeader>
<TableBody> <TableBody>
{plugins.map((plugin) => ( {plugins.map((plugin) => (

View File

@ -1,17 +1,7 @@
import { import { Button, Dropdown, DropdownItem, DropdownMenu, DropdownTrigger, Input, Tab, Tabs } from '@heroui/react'
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 { Filter, Search } from 'lucide-react' 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 { useTranslation } from 'react-i18next'
import { PluginCard } from './PluginCard' import { PluginCard } from './PluginCard'
@ -46,10 +36,11 @@ export const PluginBrowser: FC<PluginBrowserProps> = ({
const [searchQuery, setSearchQuery] = useState('') const [searchQuery, setSearchQuery] = useState('')
const [selectedCategories, setSelectedCategories] = useState<string[]>([]) const [selectedCategories, setSelectedCategories] = useState<string[]>([])
const [activeType, setActiveType] = useState<PluginType>('all') 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 [actioningPlugin, setActioningPlugin] = useState<string | null>(null)
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)
// Combine all plugins based on active type // Combine all plugins based on active type
const allPlugins = useMemo(() => { const allPlugins = useMemo(() => {
@ -95,14 +86,35 @@ export const PluginBrowser: FC<PluginBrowserProps> = ({
}) })
}, [allPlugins, searchQuery, selectedCategories]) }, [allPlugins, searchQuery, selectedCategories])
// Paginate filtered plugins // Display plugins based on displayCount
const paginatedPlugins = useMemo(() => { const displayedPlugins = useMemo(() => {
const startIndex = (currentPage - 1) * ITEMS_PER_PAGE return filteredPlugins.slice(0, displayCount)
const endIndex = startIndex + ITEMS_PER_PAGE }, [filteredPlugins, displayCount])
return filteredPlugins.slice(startIndex, endIndex)
}, [filteredPlugins, currentPage])
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 // Check if a plugin is installed
const isPluginInstalled = (plugin: PluginMetadata): boolean => { const isPluginInstalled = (plugin: PluginMetadata): boolean => {
@ -125,10 +137,9 @@ export const PluginBrowser: FC<PluginBrowserProps> = ({
setActioningPlugin(null) setActioningPlugin(null)
} }
// Reset to first page when filters change // Reset display count when filters change
const handleSearchChange = (value: string) => { const handleSearchChange = (value: string) => {
setSearchQuery(value) setSearchQuery(value)
setCurrentPage(1)
} }
const handleCategoryChange = (keys: Set<string>) => { const handleCategoryChange = (keys: Set<string>) => {
@ -138,12 +149,10 @@ export const PluginBrowser: FC<PluginBrowserProps> = ({
} else { } else {
setSelectedCategories(Array.from(keys).filter((key) => key !== 'all')) setSelectedCategories(Array.from(keys).filter((key) => key !== 'all'))
} }
setCurrentPage(1)
} }
const handleTypeChange = (type: string | number) => { const handleTypeChange = (type: string | number) => {
setActiveType(type as PluginType) setActiveType(type as PluginType)
setCurrentPage(1)
} }
const handlePluginClick = (plugin: PluginMetadata) => { const handlePluginClick = (plugin: PluginMetadata) => {
@ -159,32 +168,27 @@ export const PluginBrowser: FC<PluginBrowserProps> = ({
return ( return (
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
{/* Search and Filter */} {/* Search and Filter */}
<div className="flex gap-2"> <div className="relative flex gap-0">
<Input <Input
placeholder={t('plugins.search_placeholder')} placeholder={t('plugins.search_placeholder')}
value={searchQuery} value={searchQuery}
onValueChange={handleSearchChange} onValueChange={handleSearchChange}
startContent={<Search className="h-4 w-4 text-default-400" />} startContent={<Search className="h-4 w-4 text-default-400" />}
isClearable isClearable
classNames={{ size="md"
input: 'text-small',
inputWrapper: 'h-10'
}}
className="flex-1" className="flex-1"
/>
<Dropdown
placement="bottom-start"
classNames={{ 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> <DropdownTrigger>
<Button <Button
isIconOnly isIconOnly
variant={selectedCategories.length > 0 ? 'solid' : 'bordered'} variant={selectedCategories.length > 0 ? 'flat' : 'light'}
color={selectedCategories.length > 0 ? 'primary' : 'default'} color={selectedCategories.length > 0 ? 'primary' : 'default'}
size="md" size="sm"
className="h-10 min-w-10"> className="-translate-y-1/2 absolute top-1/2 right-2 z-10">
<Filter className="h-4 w-4" /> <Filter className="h-4 w-4" />
</Button> </Button>
</DropdownTrigger> </DropdownTrigger>
@ -225,12 +229,14 @@ export const PluginBrowser: FC<PluginBrowserProps> = ({
</div> </div>
{/* Type Tabs */} {/* Type Tabs */}
<Tabs selectedKey={activeType} onSelectionChange={handleTypeChange} variant="underlined"> <div className="-mt-3 flex justify-center">
<Tab key="all" title={t('plugins.all_types')} /> <Tabs selectedKey={activeType} onSelectionChange={handleTypeChange} variant="underlined">
<Tab key="agent" title={t('plugins.agents')} /> <Tab key="all" title={t('plugins.all_types')} />
<Tab key="command" title={t('plugins.commands')} /> <Tab key="agent" title={t('plugins.agents')} />
<Tab key="skill" title={t('plugins.skills')} /> <Tab key="command" title={t('plugins.commands')} />
</Tabs> <Tab key="skill" title={t('plugins.skills')} />
</Tabs>
</div>
{/* Result Count */} {/* Result Count */}
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
@ -238,38 +244,35 @@ export const PluginBrowser: FC<PluginBrowserProps> = ({
</div> </div>
{/* Plugin Grid */} {/* Plugin Grid */}
{paginatedPlugins.length === 0 ? ( {displayedPlugins.length === 0 ? (
<div className="flex flex-col items-center justify-center py-12 text-center"> <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-400">{t('plugins.no_results')}</p>
<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"> <>
{paginatedPlugins.map((plugin) => { <div className="grid grid-cols-1 gap-4 md:grid-cols-2">
const installed = isPluginInstalled(plugin) {displayedPlugins.map((plugin) => {
const isActioning = actioningPlugin === plugin.sourcePath const installed = isPluginInstalled(plugin)
const isActioning = actioningPlugin === plugin.sourcePath
return ( return (
<div key={`${plugin.type}-${plugin.sourcePath}`} className="h-full"> <div key={`${plugin.type}-${plugin.sourcePath}`} className="h-full">
<PluginCard <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> </div>
)} {/* Infinite scroll trigger */}
{hasMore && <div ref={observerTarget} className="h-10" />}
{/* Pagination */} </>
{totalPages > 1 && (
<div className="flex justify-center">
<Pagination total={totalPages} page={currentPage} onChange={setCurrentPage} showControls />
</div>
)} )}
{/* Plugin Detail Modal */} {/* Plugin Detail Modal */}

View File

@ -1,5 +1,6 @@
import { Button, Card, CardBody, CardFooter, CardHeader, Chip, Spinner } from '@heroui/react' import { Button, Card, CardBody, CardFooter, CardHeader, Chip, Spinner } from '@heroui/react'
import { PluginMetadata } from '@renderer/types/plugin' import { PluginMetadata } from '@renderer/types/plugin'
import { upperFirst } from 'lodash'
import { Download, Trash2 } from 'lucide-react' import { Download, Trash2 } from 'lucide-react'
import { FC } from 'react' import { FC } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
@ -18,8 +19,9 @@ export const PluginCard: FC<PluginCardProps> = ({ plugin, installed, onInstall,
return ( return (
<Card <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 isPressable
shadow="none"
onPress={onClick}> 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 gap-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" 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" className="h-4 min-w-0 flex-shrink-0 px-0.5 text-xs">
style={{ fontSize: '10px' }}> {upperFirst(plugin.type)}
{plugin.type}
</Chip> </Chip>
</div> </div>
<Chip size="sm" variant="dot" color="default"> <Chip size="sm" variant="dot" color="default">

View File

@ -9,7 +9,8 @@
"packages/shared/**/*", "packages/shared/**/*",
"scripts", "scripts",
"packages/mcp-trace/**/*", "packages/mcp-trace/**/*",
"src/renderer/src/services/traceApi.ts" ], "src/renderer/src/services/traceApi.ts"
],
"compilerOptions": { "compilerOptions": {
"composite": true, "composite": true,
"incremental": true, "incremental": true,