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

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 { Navbar, NavbarCenter } from '@renderer/components/app/Navbar'
import Scrollbar from '@renderer/components/Scrollbar' import Scrollbar from '@renderer/components/Scrollbar'
import SystemAgents from '@renderer/config/agents.json' import SystemAgents from '@renderer/config/agents.json'
@ -14,6 +14,7 @@ import styled from 'styled-components'
import { groupTranslations } from './agentGroupTranslations' import { groupTranslations } from './agentGroupTranslations'
import Agents from './Agents' import Agents from './Agents'
import AddAgentCard from './components/AddAgentCard'
import AddAgentPopup from './components/AddAgentPopup' import AddAgentPopup from './components/AddAgentPopup'
import AgentCard from './components/AgentCard' import AgentCard from './components/AgentCard'
@ -128,17 +129,17 @@ const AgentsPage: FC = () => {
<Title level={5} key={group} style={{ marginBottom: 16 }}> <Title level={5} key={group} style={{ marginBottom: 16 }}>
{localizedGroupName} {localizedGroupName}
</Title> </Title>
<Row gutter={[25, 25]}> <Row gutter={[20, 20]}>
{group === '我的' ? ( {group === '我的' ? (
<> <>
<Col span={8} xxl={6}> <Col span={6}>
<AddAgentCard onClick={() => AddAgentPopup.show()} /> <AddAgentCard onClick={() => AddAgentPopup.show()} />
</Col> </Col>
<Agents onClick={onAddAgentConfirm} cardStyle="new" /> <Agents onClick={onAddAgentConfirm} />
</> </>
) : ( ) : (
filteredAgentGroups[group]?.map((agent, index) => ( 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} /> <AgentCard onClick={() => onAddAgentConfirm(getAgentFromSystemAgent(agent))} agent={agent as any} />
</Col> </Col>
)) ))
@ -151,7 +152,7 @@ const AgentsPage: FC = () => {
}, [filteredAgentGroups, getLocalizedGroupName, onAddAgentConfirm]) }, [filteredAgentGroups, getLocalizedGroupName, onAddAgentConfirm])
return ( return (
<StyledContainer> <Container>
<Navbar> <Navbar>
<NavbarCenter style={{ borderRight: 'none', justifyContent: 'space-between' }}> <NavbarCenter style={{ borderRight: 'none', justifyContent: 'space-between' }}>
{t('agents.title')} {t('agents.title')}
@ -173,7 +174,7 @@ const AgentsPage: FC = () => {
<ContentContainer id="content-container"> <ContentContainer id="content-container">
<AssistantsContainer> <AssistantsContainer>
{tabItems.length > 0 ? ( {tabItems.length > 0 ? (
<Tabs tabPosition="left" animated items={tabItems} /> <Tabs tabPosition="right" animated items={tabItems} />
) : ( ) : (
<EmptyView> <EmptyView>
<Empty description={t('agents.search.no_results')} /> <Empty description={t('agents.search.no_results')} />
@ -181,11 +182,11 @@ const AgentsPage: FC = () => {
)} )}
</AssistantsContainer> </AssistantsContainer>
</ContentContainer> </ContentContainer>
</StyledContainer> </Container>
) )
} }
const StyledContainer = styled.div` const Container = styled.div`
display: flex; display: flex;
flex: 1; flex: 1;
flex-direction: column; flex-direction: column;
@ -199,6 +200,7 @@ const ContentContainer = styled.div`
justify-content: center; justify-content: center;
height: 100%; height: 100%;
padding: 0 10px; padding: 0 10px;
padding-left: 0;
` `
const AssistantsContainer = styled.div` const AssistantsContainer = styled.div`
@ -211,7 +213,7 @@ const AssistantsContainer = styled.div`
const TabContent = styled(Scrollbar)` const TabContent = styled(Scrollbar)`
height: calc(100vh - var(--navbar-height)); height: calc(100vh - var(--navbar-height));
padding: 10px 10px 10px 15px; padding: 10px 10px 10px 15px;
margin-right: 4px; margin-right: -4px;
overflow-x: hidden; overflow-x: hidden;
` `
@ -235,7 +237,11 @@ const Tabs = styled(TabsAntd)`
flex: 1; flex: 1;
flex-direction: row-reverse; flex-direction: row-reverse;
.ant-tabs-tabpane { .ant-tabs-tabpane {
padding-left: 0 !important; padding-right: 0 !important;
}
.ant-tabs-nav {
min-width: 120px;
max-width: 120px;
} }
.ant-tabs-nav-list { .ant-tabs-nav-list {
padding: 10px 8px; padding: 10px 8px;
@ -259,8 +265,8 @@ const Tabs = styled(TabsAntd)`
border-right: none; border-right: none;
} }
.ant-tabs-content-holder { .ant-tabs-content-holder {
border-left: none; border-left: 0.5px solid var(--color-border);
border-right: 0.5px solid var(--color-border); border-right: none;
} }
.ant-tabs-ink-bar { .ant-tabs-ink-bar {
display: none; 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 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` const Container = styled.div`
width: 100%; width: 100%;
height: 220px; height: 180px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
@ -36,7 +90,7 @@ const Container = styled.div`
&::before { &::before {
content: ''; content: '';
width: 100%; width: 100%;
height: 80px; height: 70px;
position: absolute; position: absolute;
top: 0; top: 0;
left: 0; left: 0;
@ -51,41 +105,21 @@ const Container = styled.div`
z-index: 1; z-index: 1;
} }
&:hover::before { .agent-prompt {
width: 100%; opacity: 1;
height: 100%; transform: translateY(0);
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%;
} }
` `
const EmojiContainer = styled.div` const EmojiContainer = styled.div`
width: 70px; width: 60px;
height: 70px; height: 60px;
min-width: 70px; min-width: 60px;
min-height: 70px; min-height: 60px;
background-color: var(--color-background); background-color: var(--color-background);
border-radius: 50%; border-radius: 50%;
border: 4px solid var(--color-border); border: 4px solid var(--color-border);
margin-top: 20px; margin-top: 5px;
transition: all 0.5s ease; transition: all 0.5s ease;
display: flex; display: flex;
align-items: center; align-items: center;
@ -126,7 +160,7 @@ const AgentPrompt = styled.p`
transition: all 0.5s ease; transition: all 0.5s ease;
margin: 0; margin: 0;
display: -webkit-box; display: -webkit-box;
-webkit-line-clamp: 4; -webkit-line-clamp: 2;
-webkit-box-orient: vertical; -webkit-box-orient: vertical;
overflow: hidden; overflow: hidden;
line-height: 1.4; line-height: 1.4;
@ -137,7 +171,7 @@ const BannerBackground = styled.div`
top: 0; top: 0;
left: 0; left: 0;
width: 100%; width: 100%;
height: 80px; height: 70px;
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: 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 export default AgentCard