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": {
|
"files": {
|
||||||
"ignoreUnknown": false,
|
"ignoreUnknown": false,
|
||||||
"includes": ["**"],
|
"includes": ["**", "!**/.claude/**"],
|
||||||
"maxSize": 2097152
|
"maxSize": 2097152
|
||||||
},
|
},
|
||||||
"formatter": {
|
"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": {
|
"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"
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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": "新建话题"
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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": "新增話題"
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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}
|
||||||
|
|||||||
@ -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
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -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
|
||||||
)}>
|
)}>
|
||||||
|
|||||||
@ -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>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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">
|
||||||
|
|||||||
@ -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) => (
|
||||||
|
|||||||
@ -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 */}
|
||||||
|
|||||||
@ -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">
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user