diff --git a/package.json b/package.json index 7126f239..c3de9ce1 100644 --- a/package.json +++ b/package.json @@ -87,6 +87,7 @@ "fs-extra": "^11.2.0", "got-scraping": "^4.1.1", "jsdom": "^26.0.0", + "lucide-react": "^0.487.0", "markdown-it": "^14.1.0", "officeparser": "^4.1.1", "proxy-agent": "^6.5.0", diff --git a/src/renderer/src/components/ListItem/index.tsx b/src/renderer/src/components/ListItem/index.tsx index 56c05346..2cd3aa21 100644 --- a/src/renderer/src/components/ListItem/index.tsx +++ b/src/renderer/src/components/ListItem/index.tsx @@ -4,7 +4,7 @@ import styled from 'styled-components' interface ListItemProps { active?: boolean icon?: ReactNode - title: string + title: ReactNode subtitle?: string titleStyle?: React.CSSProperties onClick?: () => void @@ -65,6 +65,7 @@ const IconWrapper = styled.span` ` const TextContainer = styled.div` + flex: 1; display: flex; flex-direction: column; overflow: hidden; diff --git a/src/renderer/src/pages/agents/AgentsPage.tsx b/src/renderer/src/pages/agents/AgentsPage.tsx index 840765e6..9aa1d878 100644 --- a/src/renderer/src/pages/agents/AgentsPage.tsx +++ b/src/renderer/src/pages/agents/AgentsPage.tsx @@ -1,79 +1,83 @@ -import { SearchOutlined } from '@ant-design/icons' +import { PlusOutlined, SearchOutlined } from '@ant-design/icons' import { Navbar, NavbarCenter } from '@renderer/components/app/Navbar' +import CustomTag from '@renderer/components/CustomTag' +import ListItem from '@renderer/components/ListItem' import Scrollbar from '@renderer/components/Scrollbar' +import { useAgents } from '@renderer/hooks/useAgents' import { createAssistantFromAgent } from '@renderer/services/AssistantService' import { Agent } from '@renderer/types' import { uuid } from '@renderer/utils' -import { Col, Empty, Input, Row, Tabs as TabsAntd, Typography } from 'antd' -import { groupBy, omit } from 'lodash' -import { FC, useCallback, useMemo, useState } from 'react' +import { Button, Empty, Flex, Input } from 'antd' +import { omit } from 'lodash' +import { FC, useCallback, useEffect, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' import ReactMarkdown from 'react-markdown' import styled from 'styled-components' -import { getAgentsFromSystemAgents, useSystemAgents } from '.' +import { groupByCategories, useSystemAgents } from '.' import { groupTranslations } from './agentGroupTranslations' +import AddAgentPopup from './components/AddAgentPopup' import AgentCard from './components/AgentCard' -import MyAgents from './components/MyAgents' - -const { Title } = Typography - -let _agentGroups: Record = {} +import { AgentGroupIcon } from './components/AgentGroupIcon' const AgentsPage: FC = () => { const [search, setSearch] = useState('') const [searchInput, setSearchInput] = useState('') + const [activeGroup, setActiveGroup] = useState('我的') + const [agentGroups, setAgentGroups] = useState>({}) const systemAgents = useSystemAgents() + const { agents: userAgents } = useAgents() - const agentGroups = useMemo(() => { - if (Object.keys(_agentGroups).length === 0) { - _agentGroups = groupBy(getAgentsFromSystemAgents(systemAgents), 'group') + useEffect(() => { + const systemAgentsGroupList = groupByCategories(systemAgents) + const agentsGroupList = { + 我的: userAgents, + 精选: [], + ...systemAgentsGroupList + } as Record + setAgentGroups(agentsGroupList) + }, [systemAgents, userAgents]) + + const filteredAgents = useMemo(() => { + let agents: Agent[] = [] + + if (search.trim()) { + const uniqueAgents = new Map() + + Object.entries(agentGroups).forEach(([, agents]) => { + agents.forEach((agent) => { + if ( + (agent.name.toLowerCase().includes(search.toLowerCase()) || + agent.description?.toLowerCase().includes(search.toLowerCase())) && + !uniqueAgents.has(agent.name) + ) { + uniqueAgents.set(agent.name, agent) + } + }) + }) + agents = Array.from(uniqueAgents.values()) + } else { + agents = agentGroups[activeGroup] || [] } - return _agentGroups - }, [systemAgents]) + return agents.filter((agent) => agent.name.toLowerCase().includes(search.toLowerCase())) + }, [agentGroups, activeGroup, search]) const { t, i18n } = useTranslation() - const filteredAgentGroups = useMemo(() => { - const groups: Record = { - 我的: [], - 精选: agentGroups['精选'] || [] - } - - if (!search.trim()) { - Object.entries(agentGroups).forEach(([group, agents]) => { - if (group !== '精选') { - groups[group] = agents - } - }) - return groups - } - - const uniqueAgents = new Map() - - Object.entries(agentGroups).forEach(([, agents]) => { - agents.forEach((agent) => { - if ( - (agent.name.toLowerCase().includes(search.toLowerCase()) || - agent.description?.toLowerCase().includes(search.toLowerCase())) && - !uniqueAgents.has(agent.name) - ) { - uniqueAgents.set(agent.name, agent) - } - }) - }) - - return { 搜索结果: Array.from(uniqueAgents.values()) } - }, [agentGroups, search]) - const onAddAgentConfirm = useCallback( (agent: Agent) => { window.modal.confirm({ title: agent.name, content: ( - - {agent.description || agent.prompt} - + + {agent.description && {agent.description}} + + {agent.prompt && ( + + {agent.prompt}{' '} + + )} + ), width: 600, icon: null, @@ -106,55 +110,33 @@ const AgentsPage: FC = () => { [i18n.language] ) - const renderAgentList = useCallback( - (agents: Agent[]) => { - return ( - - {agents.map((agent, index) => ( - - onAddAgentConfirm(getAgentFromSystemAgent(agent as any))} - agent={agent as any} - /> - - ))} - - ) - }, - [getAgentFromSystemAgent, onAddAgentConfirm] - ) - - const tabItems = useMemo(() => { - const groups = Object.keys(filteredAgentGroups) - - return groups.map((group, i) => { - const id = String(i + 1) - const localizedGroupName = getLocalizedGroupName(group) - const agents = filteredAgentGroups[group] || [] - - return { - label: localizedGroupName, - key: id, - children: ( - - - {localizedGroupName} - - {group === '我的' ? : renderAgentList(agents)} - - ) - } - }) - }, [filteredAgentGroups, getLocalizedGroupName, onAddAgentConfirm, search, renderAgentList]) - const handleSearch = () => { if (searchInput.trim() === '') { setSearch('') + setActiveGroup('我的') } else { + setActiveGroup('') setSearch(searchInput) } } + const handleSearchClear = () => { + setSearch('') + setActiveGroup('我的') + } + + const handleGroupClick = (group: string) => () => { + setSearch('') + setSearchInput('') + setActiveGroup(group) + } + + const handleAddAgent = () => { + AddAgentPopup.show().then(() => { + handleSearchClear() + }) + } + return ( @@ -163,11 +145,11 @@ const AgentsPage: FC = () => { setSearch('')} + onClear={handleSearchClear} suffix={} value={searchInput} maxLength={50} @@ -177,21 +159,78 @@ const AgentsPage: FC = () => {
- - - {Object.values(filteredAgentGroups).flat().length > 0 ? ( - search.trim() ? ( - {renderAgentList(Object.values(filteredAgentGroups).flat())} - ) : ( - - ) + +
+ + {Object.entries(agentGroups).map(([group]) => ( + + + + {getLocalizedGroupName(group)} + + { +
+ + {agentGroups[group].length} + +
+ } + + } + style={{ margin: '0 8px', paddingLeft: 16, paddingRight: 16 }} + onClick={handleGroupClick(group)}>
+ ))} +
+ + + + + {search.trim() ? ( + <> + + {search.trim()}{' '} + + ) : ( + <> + + {getLocalizedGroupName(activeGroup)} + + )} + + { + + {filteredAgents.length} + + } + + + + + {filteredAgents.length > 0 ? ( + + {filteredAgents.map((agent, index) => ( + onAddAgentConfirm(getAgentFromSystemAgent(agent))} + agent={agent} + activegroup={activeGroup} + getLocalizedGroupName={getLocalizedGroupName} + /> + ))} + ) : ( )} - - + +
) } @@ -203,42 +242,76 @@ const Container = styled.div` height: 100%; ` -const ContentContainer = styled.div` +const AgentsGroupList = styled(Scrollbar)` + min-width: 160px; + height: calc(100vh - var(--navbar-height)); display: flex; - flex: 1; - flex-direction: row; - justify-content: center; - height: 100%; - padding: 0 10px; - padding-left: 0; - border-top: 0.5px solid var(--color-border); + flex-direction: column; + gap: 8px; + padding: 8px 0; + border-right: 0.5px solid var(--color-border); + border-top-left-radius: inherit; + border-bottom-left-radius: inherit; + -ms-overflow-style: none; + scrollbar-width: none; + &::-webkit-scrollbar { + display: none; + } ` -const AssistantsContainer = styled.div` - display: flex; +const Main = styled.div` flex: 1; - flex-direction: row; - height: calc(100vh - var(--navbar-height)); + display: flex; ` -const TabContent = styled(Scrollbar)` +const AgentsListContainer = styled.div` height: calc(100vh - var(--navbar-height)); - padding: 10px 10px 10px 15px; - margin-right: -4px; - padding-bottom: 20px !important; - overflow-x: hidden; - transform: translateZ(0); - will-change: transform; - -webkit-font-smoothing: antialiased; + flex: 1; + display: flex; + flex-direction: column; +` + +const AgentsListHeader = styled.div` + display: flex; + align-items: center; + justify-content: space-between; + padding: 12px 16px 12px; +` + +const AgentsListTitle = styled.div` + font-size: 16px; + line-height: 18px; + font-weight: 500; + color: var(--color-text-1); + display: flex; + align-items: center; + gap: 8px; +` + +const AgentsList = styled(Scrollbar)` + flex: 1; + padding: 8px 16px 16px; + display: grid; + grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); + grid-auto-rows: 160px; + gap: 16px; +` + +const AgentDescription = styled.div` + color: var(--color-text-2); + font-size: 12px; ` const AgentPrompt = styled.div` max-height: 60vh; overflow-y: scroll; - max-width: 560px; + background-color: var(--color-background-soft); + padding: 8px; + border-radius: 10px; ` const EmptyView = styled.div` + height: 100%; display: flex; flex: 1; justify-content: center; @@ -247,74 +320,4 @@ const EmptyView = styled.div` color: var(--color-text-secondary); ` -const Tabs = styled(TabsAntd)<{ $language: string }>` - display: flex; - flex: 1; - flex-direction: row-reverse; - - .ant-tabs-tabpane { - padding-right: 0 !important; - } - .ant-tabs-nav { - min-width: ${({ $language }) => ($language.startsWith('zh') ? '120px' : '140px')}; - max-width: ${({ $language }) => ($language.startsWith('zh') ? '120px' : '140px')}; - position: relative; - overflow: hidden; - } - .ant-tabs-nav-list { - padding: 10px 8px; - } - .ant-tabs-nav-operations { - display: none !important; - } - .ant-tabs-tab { - margin: 0 !important; - border-radius: var(--list-item-border-radius); - margin-bottom: 5px !important; - font-size: 13px; - justify-content: left; - padding: 7px 15px !important; - border: 0.5px solid transparent; - justify-content: ${({ $language }) => ($language.startsWith('zh') ? 'center' : 'flex-start')}; - user-select: none; - transition: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1); - outline: none !important; - .ant-tabs-tab-btn { - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - max-width: 100px; - transition: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1); - outline: none !important; - } - &:hover { - color: var(--color-text) !important; - background-color: var(--color-background-soft); - } - } - .ant-tabs-tab-active { - background-color: var(--color-background-soft); - border: 0.5px solid var(--color-border); - transform: scale(1.02); - } - .ant-tabs-content-holder { - border-left: 0.5px solid var(--color-border); - border-right: none; - } - .ant-tabs-ink-bar { - display: none; - } - .ant-tabs-tab-btn:active { - color: var(--color-text) !important; - } - .ant-tabs-tab-active { - .ant-tabs-tab-btn { - color: var(--color-text) !important; - } - } - .ant-tabs-content { - transition: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1); - } -` - export default AgentsPage diff --git a/src/renderer/src/pages/agents/components/AddAgentCard.tsx b/src/renderer/src/pages/agents/components/AddAgentCard.tsx deleted file mode 100644 index ea58ffdc..00000000 --- a/src/renderer/src/pages/agents/components/AddAgentCard.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import { PlusOutlined } from '@ant-design/icons' -import { useTranslation } from 'react-i18next' -import styled from 'styled-components' - -interface AddAgentCardProps { - onClick: () => void - className?: string -} - -const AddAgentCard = ({ onClick, className }: AddAgentCardProps) => { - const { t } = useTranslation() - - return ( - - - {t('agents.add.title')} - - ) -} - -const StyledCard = styled.div` - width: 100%; - height: 180px; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - background-color: var(--color-background); - border-radius: 15px; - border: 1px dashed var(--color-border); - cursor: pointer; - transition: all 0.3s ease; - color: var(--color-text-soft); - - &:hover { - border-color: var(--color-primary); - color: var(--color-primary); - } -` - -export default AddAgentCard diff --git a/src/renderer/src/pages/agents/components/AgentCard.tsx b/src/renderer/src/pages/agents/components/AgentCard.tsx index 2f64e9aa..d322061a 100644 --- a/src/renderer/src/pages/agents/components/AgentCard.tsx +++ b/src/renderer/src/pages/agents/components/AgentCard.tsx @@ -1,78 +1,163 @@ -import { EllipsisOutlined } from '@ant-design/icons' +import { DeleteOutlined, EditOutlined, EllipsisOutlined, PlusOutlined, SortAscendingOutlined } from '@ant-design/icons' +import CustomTag from '@renderer/components/CustomTag' +import { useAgents } from '@renderer/hooks/useAgents' +import AssistantSettingsPopup from '@renderer/pages/settings/AssistantSettings' +import { createAssistantFromAgent } from '@renderer/services/AssistantService' import type { Agent } from '@renderer/types' import { getLeadingEmoji } from '@renderer/utils' -import { Dropdown } from 'antd' -import { type FC, memo } from 'react' +import { Button, Dropdown } from 'antd' +import { t } from 'i18next' +import { type FC, memo, useCallback, useEffect, useRef, useState } from 'react' import styled from 'styled-components' +import ManageAgentsPopup from './ManageAgentsPopup' + interface Props { agent: Agent + activegroup?: string onClick: () => void - contextMenu?: { - key: string - label: string - icon?: React.ReactNode - danger?: boolean - onClick: () => void - }[] - menuItems?: { - key: string - label: string - icon?: React.ReactNode - danger?: boolean - onClick: () => void - }[] + getLocalizedGroupName: (group: string) => string } -const AgentCard: FC = ({ agent, onClick, contextMenu, menuItems }) => { - const emoji = agent.emoji || getLeadingEmoji(agent.name) - const prompt = (agent.description || agent.prompt).substring(0, 100).replace(/\\n/g, '') - const content = ( - - {emoji && {emoji}} - {emoji} - {menuItems && ( - e.stopPropagation()}> - ({ - ...item, - onClick: (e) => { - e.domEvent.stopPropagation() - e.domEvent.preventDefault() - setTimeout(() => { - item.onClick() - }, 0) - } - })) - }} - trigger={['click']} - placement="bottomRight"> - - - - )} - - {agent.name} - {prompt}... - - +const AgentCard: FC = ({ agent, onClick, activegroup, getLocalizedGroupName }) => { + const { removeAgent } = useAgents() + const [isVisible, setIsVisible] = useState(false) + const cardRef = useRef(null) + + const handleDelete = useCallback( + (agent: Agent) => { + window.modal.confirm({ + centered: true, + content: t('agents.delete.popup.content'), + onOk: () => removeAgent(agent.id) + }) + }, + [removeAgent] ) - if (contextMenu) { + const menuItems = [ + { + key: 'edit', + label: t('agents.edit.title'), + icon: , + onClick: (e: any) => { + e.domEvent.stopPropagation() + AssistantSettingsPopup.show({ assistant: agent }) + } + }, + { + key: 'create', + label: t('agents.add.button'), + icon: , + onClick: (e: any) => { + e.domEvent.stopPropagation() + createAssistantFromAgent(agent) + } + }, + { + key: 'sort', + label: t('agents.sorting.title'), + icon: , + onClick: (e: any) => { + e.domEvent.stopPropagation() + ManageAgentsPopup.show() + } + }, + { + key: 'delete', + label: t('common.delete'), + icon: , + danger: true, + onClick: (e: any) => { + e.domEvent.stopPropagation() + handleDelete(agent) + } + } + ] + + useEffect(() => { + const observer = new IntersectionObserver( + (entries) => { + if (entries[0].isIntersecting) { + setIsVisible(true) + observer.disconnect() + } + }, + { threshold: 0.1 } + ) + + if (cardRef.current) { + observer.observe(cardRef.current) + } + + return () => { + observer.disconnect() + } + }, []) + + const emoji = agent.emoji || getLeadingEmoji(agent.name) + const prompt = (agent.description || agent.prompt).substring(0, 200).replace(/\\n/g, '') + + const content = ( + + {isVisible && ( + + {emoji} + + + {agent.name} + + {activegroup === '我的' && ( + + {getLocalizedGroupName('我的')} + + )} + {!!agent.group?.length && + agent.group.map((group) => ( + + {getLocalizedGroupName(group)} + + ))} + + + {activegroup === '我的' ? ( + + {emoji && {emoji}} + + { + e.stopPropagation() + e.preventDefault() + }} + color="default" + variant="filled" + shape="circle" + icon={} + /> + + + ) : ( + emoji && {emoji} + )} + + + {prompt} + + + )} + + ) + + if (activegroup === '我的') { return ( ({ - ...item, - onClick: (e) => { - e.domEvent.stopPropagation() - e.domEvent.preventDefault() - setTimeout(() => { - item.onClick() - }, 0) - } - })) + items: menuItems }} trigger={['contextMenu']}> {content} @@ -83,138 +168,153 @@ const AgentCard: FC = ({ agent, onClick, contextMenu, menuItems }) => { return content } -const Container = styled.div` - width: 100%; - height: 180px; - display: flex; - flex-direction: column; - align-items: center; - justify-content: flex-start; - text-align: center; - gap: 10px; - background-color: var(--color-background); - border-radius: 10px; +const AgentCardHeaderInfoAction = styled.div` + width: 45px; + height: 45px; position: relative; - overflow: hidden; - cursor: pointer; - border: 0.5px solid var(--color-border); - - &::before { - content: ''; - width: 100%; - height: 70px; - position: absolute; - top: 0; - left: 0; - border-top-left-radius: 8px; - border-top-right-radius: 8px; - background: var(--color-background-soft); - transition: all 0.5s ease; - border-bottom: none; - } - - * { - z-index: 1; - } - - .agent-prompt { - opacity: 1; - transform: translateY(0); - } + display: flex; + align-items: flex-start; + justify-content: flex-end; ` -const EmojiContainer = styled.div` - width: 55px; - height: 55px; - min-width: 55px; - min-height: 55px; - background-color: var(--color-background); - border-radius: 50%; - border: 4px solid var(--color-border); - margin-top: 8px; - transition: all 0.5s ease; +const HeaderInfoEmoji = styled.div` + width: 45px; + height: 45px; + border-radius: var(--list-item-border-radius); + font-size: 26px; + line-height: 1; + opacity: 0.8; + flex-shrink: 0; + opacity: 1; + transition: opacity 0.2s ease; + background-color: var(--color-background-soft); display: flex; align-items: center; justify-content: center; - font-size: 32px; +` + +const MenuButton = styled(Button)` + position: absolute; + opacity: 0; + transition: opacity 0.2s ease; +` + +const AgentCardContainer = styled.div` + border-radius: var(--list-item-border-radius); + cursor: pointer; + border: 0.5px solid var(--color-border); + padding: 16px; + overflow: hidden; + transition: + box-shadow 0.2s ease, + background-color 0.2s ease, + transform 0.2s ease; + + --shadow-color: rgba(0, 0, 0, 0.05); + box-shadow: + 0 5px 7px -3px var(--shadow-color), + 0 2px 3px -4px var(--shadow-color); + &:hover { + box-shadow: + 0 10px 15px -3px var(--shadow-color), + 0 4px 6px -4px var(--shadow-color); + transform: translateY(-2px); + + ${AgentCardHeaderInfoAction} ${HeaderInfoEmoji} { + opacity: 0; + } + ${AgentCardHeaderInfoAction} ${MenuButton} { + opacity: 1; + } + } + body[theme-mode='dark'] & { + --shadow-color: rgba(255, 255, 255, 0.02); + } +` + +const AgentCardBody = styled.div` + @keyframes fadeIn { + from { + opacity: 0; + } + to { + opacity: 1; + } + } + height: 100%; + display: flex; + flex-direction: column; + position: relative; + animation: fadeIn 0.2s ease; +` + +const AgentCardBackground = styled.div` + height: 100%; + position: absolute; + top: 0; + right: -50px; + font-size: 200px; + display: flex; + align-items: center; + justify-content: center; + pointer-events: none; + opacity: 0.1; + filter: blur(20px); + border-radius: 99px; + overflow: hidden; +` + +const AgentCardHeader = styled.div` + display: flex; + align-items: flex-start; + gap: 8px; + justify-content: flex-start; + overflow: hidden; +` + +const AgentCardHeaderInfo = styled.div` + flex: 1; + display: flex; + flex-direction: column; + gap: 7px; +` + +const AgentCardHeaderInfoTitle = styled.div` + font-size: 16px; + line-height: 1.2; + font-weight: 600; + overflow: hidden; + display: -webkit-box; + -webkit-line-clamp: 1; + -webkit-box-orient: vertical; + word-break: break-all; +` + +const AgentCardHeaderInfoTags = styled.div` + display: flex; + flex-direction: row; + gap: 5px; + flex-wrap: wrap; ` const CardInfo = styled.div` + flex: 1; display: flex; flex-direction: column; - align-items: center; - gap: 5px; - transition: all 0.5s ease; - padding: 0 8px; - width: 100%; -` - -const AgentName = styled.span` - font-weight: 600; - font-size: 16px; - color: var(--color-text); - margin-top: 5px; - line-height: 1.4; - max-width: 100%; - display: -webkit-box; - -webkit-line-clamp: 2; - -webkit-box-orient: vertical; - overflow: hidden; - word-break: break-word; -` - -const AgentPrompt = styled.p` - color: var(--color-text-soft); - font-size: 12px; - max-width: 100%; - opacity: 0; - transform: translateY(20px); - transition: all 0.5s ease; - margin: 0; - display: -webkit-box; - -webkit-line-clamp: 2; - -webkit-box-orient: vertical; - overflow: hidden; - line-height: 1.4; -` - -const BannerBackground = styled.div` - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 70px; - display: flex; - justify-content: center; - align-items: center; - font-size: 500px; - opacity: 0.1; - filter: blur(8px); - z-index: 0; - overflow: hidden; - transition: all 0.5s ease; -` - -const MenuContainer = styled.div` - position: absolute; - top: 10px; - right: 10px; - display: flex; - align-items: center; - justify-content: center; + margin-top: 16px; background-color: var(--color-background-soft); - width: 24px; - height: 24px; - border-radius: 12px; - font-size: 16px; - color: var(--color-icon); - opacity: 0; - transition: opacity 0.3s; - z-index: 2; + padding: 8px; + border-radius: 10px; +` - ${Container}:hover & { - opacity: 1; - } +const AgentPrompt = styled.div` + font-size: 12px; + display: -webkit-box; + line-height: 1.4; + -webkit-line-clamp: 3; + -webkit-box-orient: vertical; + overflow: hidden; + color: var(--color-text-2); ` export default memo(AgentCard) diff --git a/src/renderer/src/pages/agents/components/AgentGroupIcon.tsx b/src/renderer/src/pages/agents/components/AgentGroupIcon.tsx new file mode 100644 index 00000000..4078421b --- /dev/null +++ b/src/renderer/src/pages/agents/components/AgentGroupIcon.tsx @@ -0,0 +1,50 @@ +import { DynamicIcon, IconName } from 'lucide-react/dynamic' +import { FC } from 'react' + +interface Props { + groupName: string + size?: number + strokeWidth?: number +} + +export const AgentGroupIcon: FC = ({ groupName, size = 20, strokeWidth = 1.2 }) => { + const iconMap: { [key: string]: IconName } = { + 我的: 'user-check', + 精选: 'star', + 职业: 'briefcase', + 商业: 'handshake', + 工具: 'wrench', + 语言: 'languages', + 办公: 'file-text', + 通用: 'settings', + 写作: 'pen-tool', + 编程: 'code', + 情感: 'heart', + 教育: 'graduation-cap', + 创意: 'lightbulb', + 学术: 'book-open', + 设计: 'wand-sparkles', + 艺术: 'palette', + 娱乐: 'gamepad-2', + 生活: 'coffee', + 医疗: 'stethoscope', + 游戏: 'gamepad-2', + 翻译: 'languages', + 音乐: 'music', + 点评: 'message-square-more', + 文案: 'file-text', + 百科: 'book', + 健康: 'heart-pulse', + 营销: 'trending-up', + 科学: 'flask-conical', + 分析: 'bar-chart', + 法律: 'scale', + 咨询: 'messages-square', + 金融: 'banknote', + 旅游: 'plane', + 管理: 'users', + 搜索: 'search' + } as const + + return +} diff --git a/src/renderer/src/pages/agents/components/MyAgents.tsx b/src/renderer/src/pages/agents/components/MyAgents.tsx deleted file mode 100644 index ad93a825..00000000 --- a/src/renderer/src/pages/agents/components/MyAgents.tsx +++ /dev/null @@ -1,89 +0,0 @@ -import { DeleteOutlined, EditOutlined, PlusOutlined, SortAscendingOutlined } from '@ant-design/icons' -import { useAgents } from '@renderer/hooks/useAgents' -import AssistantSettingsPopup from '@renderer/pages/settings/AssistantSettings' -import { createAssistantFromAgent } from '@renderer/services/AssistantService' -import type { Agent } from '@renderer/types' -import { Col, Row } from 'antd' -import { useCallback, useMemo } from 'react' -import { useTranslation } from 'react-i18next' - -import AddAgentCard from './AddAgentCard' -import AddAgentPopup from './AddAgentPopup' -import AgentCard from './AgentCard' -import ManageAgentsPopup from './ManageAgentsPopup' - -interface Props { - onClick?: (agent: Agent) => void - search?: string -} - -const MyAgents: React.FC = ({ onClick, search }) => { - const { t } = useTranslation() - const { agents, removeAgent } = useAgents() - - const filteredAgents = useMemo(() => { - if (!search?.trim()) return agents - - return agents.filter( - (agent) => - agent.name.toLowerCase().includes(search.toLowerCase()) || - agent.description?.toLowerCase().includes(search.toLowerCase()) - ) - }, [agents, search]) - - const handleDelete = useCallback( - (agent: Agent) => { - window.modal.confirm({ - centered: true, - content: t('agents.delete.popup.content'), - onOk: () => removeAgent(agent.id) - }) - }, - [removeAgent, t] - ) - - return ( - - {filteredAgents.map((agent) => { - const menuItems = [ - { - key: 'edit', - label: t('agents.edit.title'), - icon: , - onClick: () => AssistantSettingsPopup.show({ assistant: agent }) - }, - { - key: 'create', - label: t('agents.add.button'), - icon: , - onClick: () => createAssistantFromAgent(agent) - }, - { - key: 'sort', - label: t('agents.sorting.title'), - icon: , - onClick: () => ManageAgentsPopup.show() - }, - { - key: 'delete', - label: t('common.delete'), - icon: , - danger: true, - onClick: () => handleDelete(agent) - } - ] - - return ( - - onClick?.(agent)} contextMenu={menuItems} menuItems={menuItems} /> - - ) - })} - - AddAgentPopup.show()} /> - - - ) -} - -export default MyAgents diff --git a/src/renderer/src/pages/agents/index.ts b/src/renderer/src/pages/agents/index.ts index 6069d40b..d5bd6361 100644 --- a/src/renderer/src/pages/agents/index.ts +++ b/src/renderer/src/pages/agents/index.ts @@ -22,7 +22,7 @@ export function useSystemAgents() { useEffect(() => { runAsyncFunction(async () => { - if (_agents.length > 0) return + if (!resourcesPath || _agents.length > 0) return const agents = await window.api.fs.read(resourcesPath + '/data/agents.json') _agents = JSON.parse(agents) as Agent[] setAgents(_agents) @@ -31,3 +31,20 @@ export function useSystemAgents() { return agents } + +export function groupByCategories(data: Agent[]) { + const groupedMap = new Map() + data.forEach((item) => { + item.group?.forEach((category) => { + if (!groupedMap.has(category)) { + groupedMap.set(category, []) + } + groupedMap.get(category)?.push(item) + }) + }) + const result: Record = {} + Array.from(groupedMap.entries()).forEach(([category, items]) => { + result[category] = items + }) + return result +} diff --git a/src/renderer/src/types/index.ts b/src/renderer/src/types/index.ts index 7077e559..efb8004e 100644 --- a/src/renderer/src/types/index.ts +++ b/src/renderer/src/types/index.ts @@ -45,7 +45,9 @@ export type AssistantSettings = { reasoning_effort?: 'low' | 'medium' | 'high' } -export type Agent = Omit +export type Agent = Omit & { + group?: string[] +} export type Message = { id: string diff --git a/yarn.lock b/yarn.lock index 03f1f521..8b28d187 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3980,6 +3980,7 @@ __metadata: lint-staged: "npm:^15.5.0" lodash: "npm:^4.17.21" lru-cache: "npm:^11.1.0" + lucide-react: "npm:^0.487.0" markdown-it: "npm:^14.1.0" mime: "npm:^4.0.4" npx-scope-finder: "npm:^1.2.0" @@ -10259,6 +10260,15 @@ __metadata: languageName: node linkType: hard +"lucide-react@npm:^0.487.0": + version: 0.487.0 + resolution: "lucide-react@npm:0.487.0" + peerDependencies: + react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0 + checksum: 10c0/7177778c584b8e9545957bef28e95841c4be1b3bf473f9e2e64454c3e183d7ed0bc977c9f7b5446088023c7000151b7a3b27398d4f70025bf343782192f653ca + languageName: node + linkType: hard + "magic-string@npm:^0.30.10": version: 0.30.17 resolution: "magic-string@npm:0.30.17"