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": {
"ignoreUnknown": false,
"includes": ["**"],
"includes": ["**", "!**/.claude/**"],
"maxSize": 2097152
},
"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": {
"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"
}

View File

@ -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": "新建话题"
}

View File

@ -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": "新增話題"
}

View File

@ -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}

View File

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

View File

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

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 { 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,18 +17,19 @@ 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)
onCreateAssistant()
}
const handleAddAgent = () => {
setIsPopoverOpen(false)
onAgentModalOpen()
const handleAddButtonClick = () => {
AddAssistantOrAgentPopup.show({
onSelect: (type) => {
if (type === 'assistant') {
onCreateAssistant()
} else if (type === 'agent') {
onAgentModalOpen()
}
}
})
}
const afterCreate = useCallback(
@ -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" />}>
{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 onPress={handleAddButtonClick} className="-mt-[1px] mb-[2px]">
{t('chat.add.assistant.title')}
</AddButton>
<AgentModal isOpen={isAgentModalOpen} onClose={onAgentModalClose} afterSubmit={afterCreate} />
</div>
)

View File

@ -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>

View File

@ -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">

View File

@ -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) => (

View File

@ -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 */}
<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 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,38 +244,35 @@ 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) => {
const installed = isPluginInstalled(plugin)
const isActioning = actioningPlugin === plugin.sourcePath
<>
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
{displayedPlugins.map((plugin) => {
const installed = isPluginInstalled(plugin)
const isActioning = actioningPlugin === plugin.sourcePath
return (
<div key={`${plugin.type}-${plugin.sourcePath}`} className="h-full">
<PluginCard
plugin={plugin}
installed={installed}
onInstall={() => handleInstall(plugin)}
onUninstall={() => handleUninstall(plugin)}
loading={loading || isActioning}
onClick={() => handlePluginClick(plugin)}
/>
</div>
)
})}
</div>
)}
{/* Pagination */}
{totalPages > 1 && (
<div className="flex justify-center">
<Pagination total={totalPages} page={currentPage} onChange={setCurrentPage} showControls />
</div>
return (
<div key={`${plugin.type}-${plugin.sourcePath}`} className="h-full">
<PluginCard
plugin={plugin}
installed={installed}
onInstall={() => handleInstall(plugin)}
onUninstall={() => handleUninstall(plugin)}
loading={loading || isActioning}
onClick={() => handlePluginClick(plugin)}
/>
</div>
)
})}
</div>
{/* Infinite scroll trigger */}
{hasMore && <div ref={observerTarget} className="h-10" />}
</>
)}
{/* Plugin Detail Modal */}

View File

@ -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">

View File

@ -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,