feat: improved ui layout and added reusable add agent card component
This commit is contained in:
parent
071a3950cd
commit
0d2ad2e4c3
@ -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>
|
||||
)
|
||||
})}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
41
src/renderer/src/pages/agents/components/AddAgentCard.tsx
Normal file
41
src/renderer/src/pages/agents/components/AddAgentCard.tsx
Normal 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
|
||||
@ -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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user