feat: improved ui layout and added reusable add agent card component

This commit is contained in:
kangfenmao 2024-11-01 11:00:17 +08:00
parent 071a3950cd
commit 0d2ad2e4c3
4 changed files with 179 additions and 261 deletions

View File

@ -1,26 +1,22 @@
import { DeleteOutlined, EditOutlined, PlusOutlined } from '@ant-design/icons'
import AssistantSettingsPopup from '@renderer/components/AssistantSettings'
import DragableList from '@renderer/components/DragableList'
import { useAgents } from '@renderer/hooks/useAgents'
import { createAssistantFromAgent } from '@renderer/services/AssistantService'
import { Agent } from '@renderer/types'
import { Button, Col, Typography } from 'antd'
import { useCallback, useState } from 'react'
import { Col } from 'antd'
import { useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
import AddAgentPopup from './components/AddAgentPopup'
import AgentCard from './components/AgentCard'
interface Props {
onClick?: (agent: Agent) => void
cardStyle?: 'new' | 'old'
}
const Agents: React.FC<Props> = ({ onClick, cardStyle = 'old' }) => {
const Agents: React.FC<Props> = ({ onClick }) => {
const { t } = useTranslation()
const { agents, removeAgent, updateAgents } = useAgents()
const [dragging, setDragging] = useState(false)
const { agents, removeAgent } = useAgents()
const handleDelete = useCallback(
(agent: Agent) => {
@ -33,135 +29,58 @@ const Agents: React.FC<Props> = ({ onClick, cardStyle = 'old' }) => {
[removeAgent, t]
)
if (cardStyle === 'new') {
return (
<>
{agents.map((agent) => {
const dropdownMenuItems = [
{
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: 'delete',
label: t('common.delete'),
icon: <DeleteOutlined />,
danger: true,
onClick: () => handleDelete(agent)
}
]
const contextMenuItems = [
{
label: t('agents.edit.title'),
onClick: () => AssistantSettingsPopup.show({ assistant: agent })
},
{
label: t('agents.add.button'),
onClick: () => createAssistantFromAgent(agent)
},
{
label: t('common.delete'),
onClick: () => handleDelete(agent)
}
]
return (
<Col span={8} xxl={6} key={agent.id}>
<AgentCard
agent={agent}
onClick={() => onClick?.(agent)}
contextMenu={contextMenuItems}
menuItems={dropdownMenuItems}
/>
</Col>
)
})}
</>
)
}
return (
<Container>
<div style={{ paddingBottom: dragging ? 30 : 0 }}>
<Typography.Title level={5} style={{ marginBottom: 16 }}>
{t('agents.my_agents')}
</Typography.Title>
{agents.length > 0 && (
<DragableList
list={agents}
onUpdate={updateAgents}
onDragStart={() => setDragging(true)}
onDragEnd={() => setDragging(false)}>
{(agent: Agent) => {
const dropdownMenuItems = [
{
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: 'delete',
label: t('common.delete'),
icon: <DeleteOutlined />,
danger: true,
onClick: () => handleDelete(agent)
}
]
<>
{agents.map((agent) => {
const dropdownMenuItems = [
{
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: 'delete',
label: t('common.delete'),
icon: <DeleteOutlined />,
danger: true,
onClick: () => handleDelete(agent)
}
]
const contextMenuItems = [
{
label: t('agents.edit.title'),
onClick: () => AssistantSettingsPopup.show({ assistant: agent })
},
{
label: t('agents.add.button'),
onClick: () => createAssistantFromAgent(agent)
},
{
label: t('common.delete'),
onClick: () => handleDelete(agent)
}
]
const contextMenuItems = [
{
label: t('agents.edit.title'),
onClick: () => AssistantSettingsPopup.show({ assistant: agent })
},
{
label: t('agents.add.button'),
onClick: () => createAssistantFromAgent(agent)
},
{
label: t('common.delete'),
onClick: () => handleDelete(agent)
}
]
return (
<AgentCard
agent={agent}
onClick={() => onClick?.(agent)}
contextMenu={contextMenuItems}
menuItems={dropdownMenuItems}
/>
)
}}
</DragableList>
)}
{!dragging && (
<Button
type="dashed"
icon={<PlusOutlined />}
onClick={() => AddAgentPopup.show()}
style={{ borderRadius: 20, height: 34 }}>
{t('agents.add.title')}
</Button>
)}
<div style={{ height: 10 }} />
</div>
</Container>
return (
<Col span={6} key={agent.id}>
<AgentCard
agent={agent}
onClick={() => onClick?.(agent)}
contextMenu={contextMenuItems}
menuItems={dropdownMenuItems}
/>
</Col>
)
})}
</>
)
}

View File

@ -1,4 +1,4 @@
import { PlusOutlined, SearchOutlined } from '@ant-design/icons'
import { SearchOutlined } from '@ant-design/icons'
import { Navbar, NavbarCenter } from '@renderer/components/app/Navbar'
import Scrollbar from '@renderer/components/Scrollbar'
import SystemAgents from '@renderer/config/agents.json'
@ -14,6 +14,7 @@ import styled from 'styled-components'
import { groupTranslations } from './agentGroupTranslations'
import Agents from './Agents'
import AddAgentCard from './components/AddAgentCard'
import AddAgentPopup from './components/AddAgentPopup'
import AgentCard from './components/AgentCard'
@ -128,17 +129,17 @@ const AgentsPage: FC = () => {
<Title level={5} key={group} style={{ marginBottom: 16 }}>
{localizedGroupName}
</Title>
<Row gutter={[25, 25]}>
<Row gutter={[20, 20]}>
{group === '我的' ? (
<>
<Col span={8} xxl={6}>
<Col span={6}>
<AddAgentCard onClick={() => AddAgentPopup.show()} />
</Col>
<Agents onClick={onAddAgentConfirm} cardStyle="new" />
<Agents onClick={onAddAgentConfirm} />
</>
) : (
filteredAgentGroups[group]?.map((agent, index) => (
<Col span={8} xxl={6} key={group + index}>
<Col span={6} key={group + index}>
<AgentCard onClick={() => onAddAgentConfirm(getAgentFromSystemAgent(agent))} agent={agent as any} />
</Col>
))
@ -151,7 +152,7 @@ const AgentsPage: FC = () => {
}, [filteredAgentGroups, getLocalizedGroupName, onAddAgentConfirm])
return (
<StyledContainer>
<Container>
<Navbar>
<NavbarCenter style={{ borderRight: 'none', justifyContent: 'space-between' }}>
{t('agents.title')}
@ -173,7 +174,7 @@ const AgentsPage: FC = () => {
<ContentContainer id="content-container">
<AssistantsContainer>
{tabItems.length > 0 ? (
<Tabs tabPosition="left" animated items={tabItems} />
<Tabs tabPosition="right" animated items={tabItems} />
) : (
<EmptyView>
<Empty description={t('agents.search.no_results')} />
@ -181,11 +182,11 @@ const AgentsPage: FC = () => {
)}
</AssistantsContainer>
</ContentContainer>
</StyledContainer>
</Container>
)
}
const StyledContainer = styled.div`
const Container = styled.div`
display: flex;
flex: 1;
flex-direction: column;
@ -199,6 +200,7 @@ const ContentContainer = styled.div`
justify-content: center;
height: 100%;
padding: 0 10px;
padding-left: 0;
`
const AssistantsContainer = styled.div`
@ -211,7 +213,7 @@ const AssistantsContainer = styled.div`
const TabContent = styled(Scrollbar)`
height: calc(100vh - var(--navbar-height));
padding: 10px 10px 10px 15px;
margin-right: 4px;
margin-right: -4px;
overflow-x: hidden;
`
@ -235,7 +237,11 @@ const Tabs = styled(TabsAntd)`
flex: 1;
flex-direction: row-reverse;
.ant-tabs-tabpane {
padding-left: 0 !important;
padding-right: 0 !important;
}
.ant-tabs-nav {
min-width: 120px;
max-width: 120px;
}
.ant-tabs-nav-list {
padding: 10px 8px;
@ -259,8 +265,8 @@ const Tabs = styled(TabsAntd)`
border-right: none;
}
.ant-tabs-content-holder {
border-left: none;
border-right: 0.5px solid var(--color-border);
border-left: 0.5px solid var(--color-border);
border-right: none;
}
.ant-tabs-ink-bar {
display: none;
@ -275,33 +281,4 @@ const Tabs = styled(TabsAntd)`
}
`
const AddAgentCard = styled(({ onClick, className }: { onClick: () => void; className?: string }) => {
const { t } = useTranslation()
return (
<div className={className} onClick={onClick}>
<PlusOutlined style={{ fontSize: 24 }} />
<span style={{ marginTop: 10 }}>{t('agents.add.title')}</span>
</div>
)
})`
width: 100%;
height: 220px;
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 AgentsPage

View File

@ -0,0 +1,41 @@
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

@ -17,9 +17,63 @@ interface Props {
}[]
}
const AgentCard: React.FC<Props> = ({ 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 = (
<Container onClick={onClick}>
{agent.emoji && <BannerBackground className="banner-background">{agent.emoji}</BannerBackground>}
<EmojiContainer className="emoji-container">{emoji}</EmojiContainer>
{menuItems && (
<MenuContainer onClick={(e) => e.stopPropagation()}>
<Dropdown
menu={{
items: menuItems.map((item) => ({
...item,
onClick: (e) => {
e.domEvent.stopPropagation()
e.domEvent.preventDefault()
setTimeout(() => {
item.onClick()
}, 0)
}
}))
}}
trigger={['click']}
placement="bottomRight">
<EllipsisOutlined style={{ cursor: 'pointer' }} />
</Dropdown>
</MenuContainer>
)}
<CardInfo className="card-info">
<AgentName>{agent.name}</AgentName>
<AgentPrompt className="agent-prompt">{prompt}...</AgentPrompt>
</CardInfo>
</Container>
)
if (contextMenu) {
return (
<Dropdown
menu={{
items: contextMenu.map((item) => ({
key: item.label,
label: item.label,
onClick: () => item.onClick()
}))
}}
trigger={['contextMenu']}>
{content}
</Dropdown>
)
}
return content
}
const Container = styled.div`
width: 100%;
height: 220px;
height: 180px;
display: flex;
flex-direction: column;
align-items: center;
@ -36,7 +90,7 @@ const Container = styled.div`
&::before {
content: '';
width: 100%;
height: 80px;
height: 70px;
position: absolute;
top: 0;
left: 0;
@ -51,41 +105,21 @@ const Container = styled.div`
z-index: 1;
}
&:hover::before {
width: 100%;
height: 100%;
border-radius: 15px;
}
&:hover .card-info {
transform: translateY(-15px);
padding: 0 20px;
.agent-prompt {
opacity: 1;
transform: translateY(0);
}
}
&:hover .emoji-container {
transform: scale(0.6);
margin-top: 5px;
}
&:hover .banner-background {
height: 100%;
.agent-prompt {
opacity: 1;
transform: translateY(0);
}
`
const EmojiContainer = styled.div`
width: 70px;
height: 70px;
min-width: 70px;
min-height: 70px;
width: 60px;
height: 60px;
min-width: 60px;
min-height: 60px;
background-color: var(--color-background);
border-radius: 50%;
border: 4px solid var(--color-border);
margin-top: 20px;
margin-top: 5px;
transition: all 0.5s ease;
display: flex;
align-items: center;
@ -126,7 +160,7 @@ const AgentPrompt = styled.p`
transition: all 0.5s ease;
margin: 0;
display: -webkit-box;
-webkit-line-clamp: 4;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
line-height: 1.4;
@ -137,7 +171,7 @@ const BannerBackground = styled.div`
top: 0;
left: 0;
width: 100%;
height: 80px;
height: 70px;
display: flex;
justify-content: center;
align-items: center;
@ -171,57 +205,4 @@ const MenuContainer = styled.div`
}
`
const AgentCard: React.FC<Props> = ({ agent, onClick, contextMenu, menuItems }) => {
const emoji = agent.emoji || getLeadingEmoji(agent.name)
const content = (
<Container onClick={onClick}>
{agent.emoji && <BannerBackground className="banner-background">{agent.emoji}</BannerBackground>}
<EmojiContainer className="emoji-container">{emoji}</EmojiContainer>
{menuItems && (
<MenuContainer onClick={(e) => e.stopPropagation()}>
<Dropdown
menu={{
items: menuItems.map((item) => ({
...item,
onClick: (e) => {
e.domEvent.stopPropagation()
e.domEvent.preventDefault()
setTimeout(() => {
item.onClick()
}, 0)
}
}))
}}
trigger={['click']}
placement="bottomRight">
<EllipsisOutlined style={{ cursor: 'pointer' }} />
</Dropdown>
</MenuContainer>
)}
<CardInfo className="card-info">
<AgentName>{agent.name}</AgentName>
<AgentPrompt className="agent-prompt">{(agent.description || agent.prompt).substring(0, 100)}...</AgentPrompt>
</CardInfo>
</Container>
)
if (contextMenu) {
return (
<Dropdown
menu={{
items: contextMenu.map((item) => ({
key: item.label,
label: item.label,
onClick: () => item.onClick()
}))
}}
trigger={['contextMenu']}>
{content}
</Dropdown>
)
}
return content
}
export default AgentCard