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:
parent
f39bb9869b
commit
e4514bd04c
@ -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",
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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
|
|
||||||
@ -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)
|
||||||
|
|||||||
50
src/renderer/src/pages/agents/components/AgentGroupIcon.tsx
Normal file
50
src/renderer/src/pages/agents/components/AgentGroupIcon.tsx
Normal 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} />
|
||||||
|
}
|
||||||
@ -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
|
|
||||||
@ -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
|
||||||
|
}
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
10
yarn.lock
10
yarn.lock
@ -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"
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user