refactor(AgentPage): Refactor AgentPage UI (#4737)

* refactor(AgentPage): Refactor AgentPage UI

* style(AgentCard): update HeaderInfoEmoji styling for improved layout and visual consistency

* fix(AgentCard): conditionally render HeaderInfoEmoji to prevent rendering of undefined

* feat(AgentsPage): add handleAddAgent function to streamline agent addition process

* style(AgentsPage): remove unnecessary whitespace in title rendering
This commit is contained in:
Teo 2025-04-13 09:58:46 +08:00 committed by GitHub
parent f39bb9869b
commit e4514bd04c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 561 additions and 507 deletions

View File

@ -87,6 +87,7 @@
"fs-extra": "^11.2.0", "fs-extra": "^11.2.0",
"got-scraping": "^4.1.1", "got-scraping": "^4.1.1",
"jsdom": "^26.0.0", "jsdom": "^26.0.0",
"lucide-react": "^0.487.0",
"markdown-it": "^14.1.0", "markdown-it": "^14.1.0",
"officeparser": "^4.1.1", "officeparser": "^4.1.1",
"proxy-agent": "^6.5.0", "proxy-agent": "^6.5.0",

View File

@ -4,7 +4,7 @@ import styled from 'styled-components'
interface ListItemProps { interface ListItemProps {
active?: boolean active?: boolean
icon?: ReactNode icon?: ReactNode
title: string title: ReactNode
subtitle?: string subtitle?: string
titleStyle?: React.CSSProperties titleStyle?: React.CSSProperties
onClick?: () => void onClick?: () => void
@ -65,6 +65,7 @@ const IconWrapper = styled.span`
` `
const TextContainer = styled.div` const TextContainer = styled.div`
flex: 1;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
overflow: hidden; overflow: hidden;

View File

@ -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 { 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 Scrollbar from '@renderer/components/Scrollbar'
import { useAgents } from '@renderer/hooks/useAgents'
import { createAssistantFromAgent } from '@renderer/services/AssistantService' import { createAssistantFromAgent } from '@renderer/services/AssistantService'
import { Agent } from '@renderer/types' import { Agent } from '@renderer/types'
import { uuid } from '@renderer/utils' import { uuid } from '@renderer/utils'
import { Col, Empty, Input, Row, Tabs as TabsAntd, Typography } from 'antd' import { Button, Empty, Flex, Input } from 'antd'
import { groupBy, omit } from 'lodash' import { omit } from 'lodash'
import { FC, useCallback, useMemo, useState } from 'react' import { FC, useCallback, useEffect, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import ReactMarkdown from 'react-markdown' import ReactMarkdown from 'react-markdown'
import styled from 'styled-components' import styled from 'styled-components'
import { getAgentsFromSystemAgents, useSystemAgents } from '.' import { groupByCategories, useSystemAgents } from '.'
import { groupTranslations } from './agentGroupTranslations' import { groupTranslations } from './agentGroupTranslations'
import AddAgentPopup from './components/AddAgentPopup'
import AgentCard from './components/AgentCard' import AgentCard from './components/AgentCard'
import MyAgents from './components/MyAgents' import { AgentGroupIcon } from './components/AgentGroupIcon'
const { Title } = Typography
let _agentGroups: Record<string, Agent[]> = {}
const AgentsPage: FC = () => { const AgentsPage: FC = () => {
const [search, setSearch] = useState('') const [search, setSearch] = useState('')
const [searchInput, setSearchInput] = useState('') const [searchInput, setSearchInput] = useState('')
const [activeGroup, setActiveGroup] = useState('我的')
const [agentGroups, setAgentGroups] = useState<Record<string, Agent[]>>({})
const systemAgents = useSystemAgents() const systemAgents = useSystemAgents()
const { agents: userAgents } = useAgents()
const agentGroups = useMemo(() => { useEffect(() => {
if (Object.keys(_agentGroups).length === 0) { const systemAgentsGroupList = groupByCategories(systemAgents)
_agentGroups = groupBy(getAgentsFromSystemAgents(systemAgents), 'group') const agentsGroupList = {
我的: userAgents,
: [],
...systemAgentsGroupList
} as Record<string, Agent[]>
setAgentGroups(agentsGroupList)
}, [systemAgents, userAgents])
const filteredAgents = useMemo(() => {
let agents: Agent[] = []
if (search.trim()) {
const uniqueAgents = new Map<string, Agent>()
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 return agents.filter((agent) => agent.name.toLowerCase().includes(search.toLowerCase()))
}, [systemAgents]) }, [agentGroups, activeGroup, search])
const { t, i18n } = useTranslation() const { t, i18n } = useTranslation()
const filteredAgentGroups = useMemo(() => {
const groups: Record<string, Agent[]> = {
: [],
精选: agentGroups['精选'] || []
}
if (!search.trim()) {
Object.entries(agentGroups).forEach(([group, agents]) => {
if (group !== '精选') {
groups[group] = agents
}
})
return groups
}
const uniqueAgents = new Map<string, Agent>()
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( const onAddAgentConfirm = useCallback(
(agent: Agent) => { (agent: Agent) => {
window.modal.confirm({ window.modal.confirm({
title: agent.name, title: agent.name,
content: ( content: (
<AgentPrompt> <Flex gap={16} vertical style={{ width: 'calc(100% + 12px)' }}>
<ReactMarkdown className="markdown">{agent.description || agent.prompt}</ReactMarkdown> {agent.description && <AgentDescription>{agent.description}</AgentDescription>}
</AgentPrompt>
{agent.prompt && (
<AgentPrompt>
<ReactMarkdown className="markdown">{agent.prompt}</ReactMarkdown>{' '}
</AgentPrompt>
)}
</Flex>
), ),
width: 600, width: 600,
icon: null, icon: null,
@ -106,55 +110,33 @@ const AgentsPage: FC = () => {
[i18n.language] [i18n.language]
) )
const renderAgentList = useCallback(
(agents: Agent[]) => {
return (
<Row gutter={[20, 20]}>
{agents.map((agent, index) => (
<Col span={6} key={agent.id || index}>
<AgentCard
onClick={() => onAddAgentConfirm(getAgentFromSystemAgent(agent as any))}
agent={agent as any}
/>
</Col>
))}
</Row>
)
},
[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: (
<TabContent key={group}>
<Title level={5} key={group} style={{ marginBottom: 10 }}>
{localizedGroupName}
</Title>
{group === '我的' ? <MyAgents onClick={onAddAgentConfirm} search={search} /> : renderAgentList(agents)}
</TabContent>
)
}
})
}, [filteredAgentGroups, getLocalizedGroupName, onAddAgentConfirm, search, renderAgentList])
const handleSearch = () => { const handleSearch = () => {
if (searchInput.trim() === '') { if (searchInput.trim() === '') {
setSearch('') setSearch('')
setActiveGroup('我的')
} else { } else {
setActiveGroup('')
setSearch(searchInput) setSearch(searchInput)
} }
} }
const handleSearchClear = () => {
setSearch('')
setActiveGroup('我的')
}
const handleGroupClick = (group: string) => () => {
setSearch('')
setSearchInput('')
setActiveGroup(group)
}
const handleAddAgent = () => {
AddAgentPopup.show().then(() => {
handleSearchClear()
})
}
return ( return (
<Container> <Container>
<Navbar> <Navbar>
@ -163,11 +145,11 @@ const AgentsPage: FC = () => {
<Input <Input
placeholder={t('common.search')} placeholder={t('common.search')}
className="nodrag" className="nodrag"
style={{ width: '30%', height: 28 }} style={{ width: '30%', height: 28, borderRadius: 15, paddingLeft: 12 }}
size="small" size="small"
variant="filled" variant="filled"
allowClear allowClear
onClear={() => setSearch('')} onClear={handleSearchClear}
suffix={<SearchOutlined onClick={handleSearch} />} suffix={<SearchOutlined onClick={handleSearch} />}
value={searchInput} value={searchInput}
maxLength={50} maxLength={50}
@ -177,21 +159,78 @@ const AgentsPage: FC = () => {
<div style={{ width: 80 }} /> <div style={{ width: 80 }} />
</NavbarCenter> </NavbarCenter>
</Navbar> </Navbar>
<ContentContainer id="content-container">
<AssistantsContainer> <Main id="content-container">
{Object.values(filteredAgentGroups).flat().length > 0 ? ( <AgentsGroupList>
search.trim() ? ( {Object.entries(agentGroups).map(([group]) => (
<TabContent>{renderAgentList(Object.values(filteredAgentGroups).flat())}</TabContent> <ListItem
) : ( active={activeGroup === group && !search.trim()}
<Tabs tabPosition="right" animated={false} items={tabItems} $language={i18n.language} /> key={group}
) title={
<Flex gap={16} align="center" justify="space-between">
<Flex gap={10} align="center">
<AgentGroupIcon groupName={group} />
{getLocalizedGroupName(group)}
</Flex>
{
<div style={{ minWidth: 40, textAlign: 'center' }}>
<CustomTag color="#A0A0A0" size={8}>
{agentGroups[group].length}
</CustomTag>
</div>
}
</Flex>
}
style={{ margin: '0 8px', paddingLeft: 16, paddingRight: 16 }}
onClick={handleGroupClick(group)}></ListItem>
))}
</AgentsGroupList>
<AgentsListContainer>
<AgentsListHeader>
<AgentsListTitle>
{search.trim() ? (
<>
<AgentGroupIcon groupName="搜索" size={24} />
{search.trim()}{' '}
</>
) : (
<>
<AgentGroupIcon groupName={activeGroup} size={24} />
{getLocalizedGroupName(activeGroup)}
</>
)}
{
<CustomTag color="#A0A0A0" size={10}>
{filteredAgents.length}
</CustomTag>
}
</AgentsListTitle>
<Button type="text" onClick={handleAddAgent} icon={<PlusOutlined />}>
{t('agents.add.title')}
</Button>
</AgentsListHeader>
{filteredAgents.length > 0 ? (
<AgentsList>
{filteredAgents.map((agent, index) => (
<AgentCard
key={agent.id || index}
onClick={() => onAddAgentConfirm(getAgentFromSystemAgent(agent))}
agent={agent}
activegroup={activeGroup}
getLocalizedGroupName={getLocalizedGroupName}
/>
))}
</AgentsList>
) : ( ) : (
<EmptyView> <EmptyView>
<Empty description={t('agents.search.no_results')} /> <Empty description={t('agents.search.no_results')} />
</EmptyView> </EmptyView>
)} )}
</AssistantsContainer> </AgentsListContainer>
</ContentContainer> </Main>
</Container> </Container>
) )
} }
@ -203,42 +242,76 @@ const Container = styled.div`
height: 100%; height: 100%;
` `
const ContentContainer = styled.div` const AgentsGroupList = styled(Scrollbar)`
min-width: 160px;
height: calc(100vh - var(--navbar-height));
display: flex; display: flex;
flex: 1; flex-direction: column;
flex-direction: row; gap: 8px;
justify-content: center; padding: 8px 0;
height: 100%; border-right: 0.5px solid var(--color-border);
padding: 0 10px; border-top-left-radius: inherit;
padding-left: 0; border-bottom-left-radius: inherit;
border-top: 0.5px solid var(--color-border); -ms-overflow-style: none;
scrollbar-width: none;
&::-webkit-scrollbar {
display: none;
}
` `
const AssistantsContainer = styled.div` const Main = styled.div`
display: flex;
flex: 1; flex: 1;
flex-direction: row; display: flex;
height: calc(100vh - var(--navbar-height));
` `
const TabContent = styled(Scrollbar)` const AgentsListContainer = styled.div`
height: calc(100vh - var(--navbar-height)); height: calc(100vh - var(--navbar-height));
padding: 10px 10px 10px 15px; flex: 1;
margin-right: -4px; display: flex;
padding-bottom: 20px !important; flex-direction: column;
overflow-x: hidden; `
transform: translateZ(0);
will-change: transform; const AgentsListHeader = styled.div`
-webkit-font-smoothing: antialiased; 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` const AgentPrompt = styled.div`
max-height: 60vh; max-height: 60vh;
overflow-y: scroll; overflow-y: scroll;
max-width: 560px; background-color: var(--color-background-soft);
padding: 8px;
border-radius: 10px;
` `
const EmptyView = styled.div` const EmptyView = styled.div`
height: 100%;
display: flex; display: flex;
flex: 1; flex: 1;
justify-content: center; justify-content: center;
@ -247,74 +320,4 @@ const EmptyView = styled.div`
color: var(--color-text-secondary); 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 export default AgentsPage

View File

@ -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 (
<StyledCard className={className} onClick={onClick}>
<PlusOutlined style={{ fontSize: 24 }} />
<span style={{ marginTop: 10 }}>{t('agents.add.title')}</span>
</StyledCard>
)
}
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

View File

@ -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 type { Agent } from '@renderer/types'
import { getLeadingEmoji } from '@renderer/utils' import { getLeadingEmoji } from '@renderer/utils'
import { Dropdown } from 'antd' import { Button, Dropdown } from 'antd'
import { type FC, memo } from 'react' import { t } from 'i18next'
import { type FC, memo, useCallback, useEffect, useRef, useState } from 'react'
import styled from 'styled-components' import styled from 'styled-components'
import ManageAgentsPopup from './ManageAgentsPopup'
interface Props { interface Props {
agent: Agent agent: Agent
activegroup?: string
onClick: () => void onClick: () => void
contextMenu?: { getLocalizedGroupName: (group: string) => string
key: string
label: string
icon?: React.ReactNode
danger?: boolean
onClick: () => void
}[]
menuItems?: {
key: string
label: string
icon?: React.ReactNode
danger?: boolean
onClick: () => void
}[]
} }
const AgentCard: FC<Props> = ({ agent, onClick, contextMenu, menuItems }) => { const AgentCard: FC<Props> = ({ agent, onClick, activegroup, getLocalizedGroupName }) => {
const emoji = agent.emoji || getLeadingEmoji(agent.name) const { removeAgent } = useAgents()
const prompt = (agent.description || agent.prompt).substring(0, 100).replace(/\\n/g, '') const [isVisible, setIsVisible] = useState(false)
const content = ( const cardRef = useRef<HTMLDivElement>(null)
<Container onClick={onClick}>
{emoji && <BannerBackground className="banner-background">{emoji}</BannerBackground>} const handleDelete = useCallback(
<EmojiContainer className="emoji-container">{emoji}</EmojiContainer> (agent: Agent) => {
{menuItems && ( window.modal.confirm({
<MenuContainer onClick={(e) => e.stopPropagation()}> centered: true,
<Dropdown content: t('agents.delete.popup.content'),
menu={{ onOk: () => removeAgent(agent.id)
items: menuItems.map((item) => ({ })
...item, },
onClick: (e) => { [removeAgent]
e.domEvent.stopPropagation()
e.domEvent.preventDefault()
setTimeout(() => {
item.onClick()
}, 0)
}
}))
}}
trigger={['click']}
placement="bottomRight">
<EllipsisOutlined style={{ cursor: 'pointer', fontSize: 20 }} />
</Dropdown>
</MenuContainer>
)}
<CardInfo className="card-info">
<AgentName>{agent.name}</AgentName>
<AgentPrompt className="agent-prompt">{prompt}...</AgentPrompt>
</CardInfo>
</Container>
) )
if (contextMenu) { const menuItems = [
{
key: 'edit',
label: t('agents.edit.title'),
icon: <EditOutlined />,
onClick: (e: any) => {
e.domEvent.stopPropagation()
AssistantSettingsPopup.show({ assistant: agent })
}
},
{
key: 'create',
label: t('agents.add.button'),
icon: <PlusOutlined />,
onClick: (e: any) => {
e.domEvent.stopPropagation()
createAssistantFromAgent(agent)
}
},
{
key: 'sort',
label: t('agents.sorting.title'),
icon: <SortAscendingOutlined />,
onClick: (e: any) => {
e.domEvent.stopPropagation()
ManageAgentsPopup.show()
}
},
{
key: 'delete',
label: t('common.delete'),
icon: <DeleteOutlined />,
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 = (
<AgentCardContainer onClick={onClick} ref={cardRef}>
{isVisible && (
<AgentCardBody>
<AgentCardBackground>{emoji}</AgentCardBackground>
<AgentCardHeader>
<AgentCardHeaderInfo>
<AgentCardHeaderInfoTitle>{agent.name}</AgentCardHeaderInfoTitle>
<AgentCardHeaderInfoTags>
{activegroup === '我的' && (
<CustomTag color="#A0A0A0" size={11}>
{getLocalizedGroupName('我的')}
</CustomTag>
)}
{!!agent.group?.length &&
agent.group.map((group) => (
<CustomTag key={group} color="#A0A0A0" size={11}>
{getLocalizedGroupName(group)}
</CustomTag>
))}
</AgentCardHeaderInfoTags>
</AgentCardHeaderInfo>
{activegroup === '我的' ? (
<AgentCardHeaderInfoAction>
{emoji && <HeaderInfoEmoji>{emoji}</HeaderInfoEmoji>}
<Dropdown
menu={{
items: menuItems
}}
trigger={['click']}
placement="bottomRight">
<MenuButton
onClick={(e) => {
e.stopPropagation()
e.preventDefault()
}}
color="default"
variant="filled"
shape="circle"
icon={<EllipsisOutlined />}
/>
</Dropdown>
</AgentCardHeaderInfoAction>
) : (
emoji && <HeaderInfoEmoji>{emoji}</HeaderInfoEmoji>
)}
</AgentCardHeader>
<CardInfo>
<AgentPrompt>{prompt}</AgentPrompt>
</CardInfo>
</AgentCardBody>
)}
</AgentCardContainer>
)
if (activegroup === '我的') {
return ( return (
<Dropdown <Dropdown
menu={{ menu={{
items: contextMenu.map((item) => ({ items: menuItems
...item,
onClick: (e) => {
e.domEvent.stopPropagation()
e.domEvent.preventDefault()
setTimeout(() => {
item.onClick()
}, 0)
}
}))
}} }}
trigger={['contextMenu']}> trigger={['contextMenu']}>
{content} {content}
@ -83,138 +168,153 @@ const AgentCard: FC<Props> = ({ agent, onClick, contextMenu, menuItems }) => {
return content return content
} }
const Container = styled.div` const AgentCardHeaderInfoAction = styled.div`
width: 100%; width: 45px;
height: 180px; height: 45px;
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;
position: relative; position: relative;
overflow: hidden; display: flex;
cursor: pointer; align-items: flex-start;
border: 0.5px solid var(--color-border); justify-content: flex-end;
&::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);
}
` `
const EmojiContainer = styled.div` const HeaderInfoEmoji = styled.div`
width: 55px; width: 45px;
height: 55px; height: 45px;
min-width: 55px; border-radius: var(--list-item-border-radius);
min-height: 55px; font-size: 26px;
background-color: var(--color-background); line-height: 1;
border-radius: 50%; opacity: 0.8;
border: 4px solid var(--color-border); flex-shrink: 0;
margin-top: 8px; opacity: 1;
transition: all 0.5s ease; transition: opacity 0.2s ease;
background-color: var(--color-background-soft);
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: 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` const CardInfo = styled.div`
flex: 1;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; margin-top: 16px;
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;
background-color: var(--color-background-soft); background-color: var(--color-background-soft);
width: 24px; padding: 8px;
height: 24px; border-radius: 10px;
border-radius: 12px; `
font-size: 16px;
color: var(--color-icon);
opacity: 0;
transition: opacity 0.3s;
z-index: 2;
${Container}:hover & { const AgentPrompt = styled.div`
opacity: 1; 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) export default memo(AgentCard)

View File

@ -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<Props> = ({ 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 <DynamicIcon name={iconMap[groupName] || 'bot-message-square'} size={size} strokeWidth={strokeWidth} />
}

View File

@ -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<Props> = ({ 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 (
<Row gutter={[20, 20]}>
{filteredAgents.map((agent) => {
const menuItems = [
{
key: 'edit',
label: t('agents.edit.title'),
icon: <EditOutlined />,
onClick: () => AssistantSettingsPopup.show({ assistant: agent })
},
{
key: 'create',
label: t('agents.add.button'),
icon: <PlusOutlined />,
onClick: () => createAssistantFromAgent(agent)
},
{
key: 'sort',
label: t('agents.sorting.title'),
icon: <SortAscendingOutlined />,
onClick: () => ManageAgentsPopup.show()
},
{
key: 'delete',
label: t('common.delete'),
icon: <DeleteOutlined />,
danger: true,
onClick: () => handleDelete(agent)
}
]
return (
<Col span={6} key={agent.id}>
<AgentCard agent={agent} onClick={() => onClick?.(agent)} contextMenu={menuItems} menuItems={menuItems} />
</Col>
)
})}
<Col span={6}>
<AddAgentCard onClick={() => AddAgentPopup.show()} />
</Col>
</Row>
)
}
export default MyAgents

View File

@ -22,7 +22,7 @@ export function useSystemAgents() {
useEffect(() => { useEffect(() => {
runAsyncFunction(async () => { runAsyncFunction(async () => {
if (_agents.length > 0) return if (!resourcesPath || _agents.length > 0) return
const agents = await window.api.fs.read(resourcesPath + '/data/agents.json') const agents = await window.api.fs.read(resourcesPath + '/data/agents.json')
_agents = JSON.parse(agents) as Agent[] _agents = JSON.parse(agents) as Agent[]
setAgents(_agents) setAgents(_agents)
@ -31,3 +31,20 @@ export function useSystemAgents() {
return agents return agents
} }
export function groupByCategories(data: Agent[]) {
const groupedMap = new Map<string, Agent[]>()
data.forEach((item) => {
item.group?.forEach((category) => {
if (!groupedMap.has(category)) {
groupedMap.set(category, [])
}
groupedMap.get(category)?.push(item)
})
})
const result: Record<string, Agent[]> = {}
Array.from(groupedMap.entries()).forEach(([category, items]) => {
result[category] = items
})
return result
}

View File

@ -45,7 +45,9 @@ export type AssistantSettings = {
reasoning_effort?: 'low' | 'medium' | 'high' reasoning_effort?: 'low' | 'medium' | 'high'
} }
export type Agent = Omit<Assistant, 'model'> export type Agent = Omit<Assistant, 'model'> & {
group?: string[]
}
export type Message = { export type Message = {
id: string id: string

View File

@ -3980,6 +3980,7 @@ __metadata:
lint-staged: "npm:^15.5.0" lint-staged: "npm:^15.5.0"
lodash: "npm:^4.17.21" lodash: "npm:^4.17.21"
lru-cache: "npm:^11.1.0" lru-cache: "npm:^11.1.0"
lucide-react: "npm:^0.487.0"
markdown-it: "npm:^14.1.0" markdown-it: "npm:^14.1.0"
mime: "npm:^4.0.4" mime: "npm:^4.0.4"
npx-scope-finder: "npm:^1.2.0" npx-scope-finder: "npm:^1.2.0"
@ -10259,6 +10260,15 @@ __metadata:
languageName: node languageName: node
linkType: hard 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": "magic-string@npm:^0.30.10":
version: 0.30.17 version: 0.30.17
resolution: "magic-string@npm:0.30.17" resolution: "magic-string@npm:0.30.17"