Agents Page Upgrade

1. Simplified the layout of the agents page for improved user experience.

2. Enhanced the design of agent cards for a more visually appealing look.
This commit is contained in:
首都爱护动物协会 2024-10-31 03:25:09 +08:00 committed by 亢奋猫
parent 76b9e1a65e
commit d7b459dcee
4 changed files with 446 additions and 229 deletions

View File

@ -1,57 +1,49 @@
import { throttle } from 'lodash'
import { FC, forwardRef, useCallback, useEffect, useRef, useState } from 'react'
import { forwardRef } from 'react'
import styled from 'styled-components'
interface Props extends React.HTMLAttributes<HTMLDivElement> {
right?: boolean
ref?: any
interface Props {
children?: React.ReactNode
className?: string
$isScrolling?: boolean
$right?: boolean
}
const Scrollbar: FC<Props> = forwardRef<HTMLDivElement, Props>((props, ref) => {
const [isScrolling, setIsScrolling] = useState(false)
const timeoutRef = useRef<NodeJS.Timeout | null>(null)
const ScrollbarContainer = styled.div<{ $isScrolling?: boolean; $right?: boolean }>`
overflow-y: auto;
overflow-x: hidden;
height: 100%;
const handleScroll = useCallback(
throttle(() => {
setIsScrolling(true)
&::-webkit-scrollbar {
width: 6px;
height: 6px;
}
if (timeoutRef.current) {
clearTimeout(timeoutRef.current)
}
&::-webkit-scrollbar-track {
border-radius: 3px;
background: transparent;
${({ $right }) => $right && `margin-right: 4px;`}
}
timeoutRef.current = setTimeout(() => setIsScrolling(false), 1500) // 增加到 2 秒
}, 200),
[]
)
&::-webkit-scrollbar-thumb {
border-radius: 3px;
background: ${({ $isScrolling }) =>
$isScrolling ? 'var(--color-scrollbar-thumb)' : 'var(--color-scrollbar-track)'};
transition: all 0.2s ease-in-out;
}
useEffect(() => {
return () => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current)
}
}
}, [])
&:hover::-webkit-scrollbar-thumb {
background: var(--color-scrollbar-thumb);
}
`
const Scrollbar = forwardRef<HTMLDivElement, Props>(({ children, className, $isScrolling, $right }, ref) => {
return (
<Container {...props} isScrolling={isScrolling} onScroll={handleScroll} ref={ref}>
{props.children}
</Container>
<ScrollbarContainer ref={ref} className={className} $isScrolling={$isScrolling} $right={$right}>
{children}
</ScrollbarContainer>
)
})
Scrollbar.displayName = 'Scrollbar'
const Container = styled.div<{ isScrolling: boolean; right?: boolean }>`
overflow-y: auto;
&::-webkit-scrollbar-thumb {
transition: background 2s ease;
background: ${(props) =>
props.isScrolling ? `var(--color-scrollbar-thumb${props.right ? '-right' : ''})` : 'transparent'};
&:hover {
background: ${(props) =>
props.isScrolling ? `var(--color-scrollbar-thumb${props.right ? '-right' : ''}-hover)` : 'transparent'};
}
}
`
export default Scrollbar

View File

@ -1,168 +1,199 @@
import { DeleteOutlined, EditOutlined, MoreOutlined, PlusOutlined } from '@ant-design/icons'
import { DeleteOutlined, EditOutlined, PlusOutlined } from '@ant-design/icons'
import AssistantSettingsPopup from '@renderer/components/AssistantSettings'
import DragableList from '@renderer/components/DragableList'
import { HStack } from '@renderer/components/Layout'
import Scrollbar from '@renderer/components/Scrollbar'
import { useAgents } from '@renderer/hooks/useAgents'
import { createAssistantFromAgent } from '@renderer/services/AssistantService'
import { Agent } from '@renderer/types'
import { Button, Dropdown, Typography } from 'antd'
import { ItemType } from 'antd/es/menu/interface'
import { Button, Col, Typography } from 'antd'
import { useCallback, useState } 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
onClick?: (agent: Agent) => void
cardStyle?: 'new' | 'old'
}
const Agents: React.FC<Props> = ({ onClick }) => {
const Agents: React.FC<Props> = ({ onClick, cardStyle = 'old' }) => {
const { t } = useTranslation()
const { agents, removeAgent, updateAgents } = useAgents()
const [dragging, setDragging] = useState(false)
const getMenuItems = useCallback(
(agent: Agent) =>
[
{
label: t('agents.edit.title'),
key: 'edit',
icon: <EditOutlined />,
onClick: () => AssistantSettingsPopup.show({ assistant: agent })
},
{
label: t('agents.add.button'),
key: 'create',
icon: <PlusOutlined />,
onClick: () => createAssistantFromAgent(agent)
},
{ type: 'divider' },
{
label: t('common.delete'),
key: 'delete',
icon: <DeleteOutlined />,
danger: true,
onClick: () => {
window.modal.confirm({
centered: true,
content: t('agents.delete.popup.content'),
onOk: () => removeAgent(agent.id)
})
}
}
] as ItemType[],
const handleDelete = useCallback(
(agent: Agent) => {
window.modal.confirm({
centered: true,
content: t('agents.delete.popup.content'),
onOk: () => removeAgent(agent.id)
})
},
[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} key={agent.id}>
<AgentCard
agent={agent}
onClick={() => onClick?.(agent)}
contextMenu={contextMenuItems}
menuItems={dropdownMenuItems}
/>
</Col>
)
})}
</>
)
}
return (
<Container 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) => (
<Dropdown menu={{ items: getMenuItems(agent) }} trigger={['contextMenu']}>
<AgentItem onClick={() => onClick(agent)}>
<HStack alignItems="center" justifyContent="space-between" h="36px">
<AgentItemName className="text-nowrap">
{agent.emoji} {agent.name}
</AgentItemName>
<ActionButton className="actions" gap="15px" onClick={(e) => e.stopPropagation()}>
<Dropdown menu={{ items: getMenuItems(agent) }} trigger={['hover']}>
<MoreOutlined style={{ cursor: 'pointer' }} />
</Dropdown>
</ActionButton>
</HStack>
<AgentItemPrompt>{agent.prompt}</AgentItemPrompt>
</AgentItem>
</Dropdown>
)}
</DragableList>
)}
{!dragging && (
<Button
type="dashed"
icon={<PlusOutlined />}
onClick={() => AddAgentPopup.show()}
style={{ borderRadius: 20, height: 34 }}>
{t('agents.add.title')}
</Button>
)}
<div style={{ height: 10 }} />
<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)
}
]
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>
)
}
const Container = styled(Scrollbar)`
const Container = styled.div`
padding: 10px 15px;
display: flex;
flex-direction: column;
min-height: calc(100vh - var(--navbar-height));
min-width: var(--assistants-width);
max-width: var(--assistants-width);
`
overflow-y: auto;
overflow-x: hidden;
const AgentItem = styled.div`
display: flex;
flex-direction: column;
padding: 0 12px;
min-height: 72px;
border-radius: 10px;
user-select: none;
margin-bottom: 15px;
padding-bottom: 10px;
border: 0.5px solid var(--color-border);
transition: all 0.2s ease-in-out;
cursor: pointer;
&:hover {
.actions {
display: flex;
}
&::-webkit-scrollbar {
width: 6px;
height: 6px;
}
&:hover {
border: 0.5px solid var(--color-primary);
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
&::-webkit-scrollbar-track {
border-radius: 3px;
background: transparent;
}
`
const AgentItemName = styled.div`
display: flex;
flex-direction: row;
align-items: center;
`
&::-webkit-scrollbar-thumb {
border-radius: 3px;
background: var(--color-scrollbar-thumb);
transition: all 0.2s ease-in-out;
}
const AgentItemPrompt = styled.div`
font-size: 12px;
color: var(--color-text-soft);
margin-top: -5px;
color: var(--color-text-3);
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
white-space: normal;
word-wrap: break-word;
line-height: 16px;
`
const ActionButton = styled(HStack)`
align-items: center;
justify-content: center;
display: none;
background-color: var(--color-background-soft);
width: 24px;
height: 24px;
border-radius: 12px;
font-size: 16px;
color: var(--color-icon);
&:hover::-webkit-scrollbar-thumb {
background: var(--color-scrollbar-thumb);
}
`
export default Agents

View File

@ -1,4 +1,4 @@
import { SearchOutlined } from '@ant-design/icons'
import { PlusOutlined, 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'
@ -13,6 +13,7 @@ import ReactMarkdown from 'react-markdown'
import styled from 'styled-components'
import Agents from './Agents'
import AddAgentPopup from './components/AddAgentPopup'
import AgentCard from './components/AgentCard'
const { Title } = Typography
@ -43,9 +44,15 @@ const AgentsPage: FC = () => {
const { t } = useTranslation()
const filteredAgentGroups = useMemo(() => {
if (!search.trim()) return agentGroups
const groups = search.trim() ? {} : { : [] }
if (!search.trim()) {
Object.entries(agentGroups).forEach(([group, agents]) => {
groups[group] = agents
})
return groups
}
const filtered = {}
Object.entries(agentGroups).forEach(([group, agents]) => {
const filteredAgents = agents.filter(
(agent) =>
@ -53,10 +60,10 @@ const AgentsPage: FC = () => {
agent.description?.toLowerCase().includes(search.toLowerCase())
)
if (filteredAgents.length > 0) {
filtered[group] = filteredAgents
groups[group] = filteredAgents
}
})
return filtered
return groups
}, [agentGroups, search])
const getAgentName = (agent: Agent) => {
@ -97,7 +104,9 @@ const AgentsPage: FC = () => {
const tabItems = useMemo(() => {
let groups = Object.keys(filteredAgentGroups)
groups = groups.includes('办公') ? ['办公', ...groups.filter((g) => g !== '办公')] : groups
groups = groups.filter((g) => g !== '我的' && g !== '办公')
groups = ['我的', '办公', ...groups]
return groups.map((group, i) => {
const id = String(i + 1)
return {
@ -108,14 +117,21 @@ const AgentsPage: FC = () => {
<Title level={5} key={group} style={{ marginBottom: 16 }}>
{group}
</Title>
<Row gutter={16}>
{filteredAgentGroups[group].map((agent, index) => {
return (
<Row gutter={[32, 32]}>
{group === '我的' ? (
<>
<Col span={8}>
<AddAgentCard onClick={() => AddAgentPopup.show()} />
</Col>
<Agents onClick={onAddAgentConfirm} cardStyle="new" />
</>
) : (
filteredAgentGroups[group]?.map((agent, index) => (
<Col span={8} key={group + index}>
<AgentCard onClick={() => onAddAgentConfirm(getAgentFromSystemAgent(agent))} agent={agent as any} />
</Col>
)
})}
))
)}
</Row>
</TabContent>
)
@ -124,7 +140,7 @@ const AgentsPage: FC = () => {
}, [filteredAgentGroups, onAddAgentConfirm])
return (
<Container>
<StyledContainer>
<Navbar>
<NavbarCenter style={{ borderRight: 'none', justifyContent: 'space-between' }}>
{t('agents.title')}
@ -145,7 +161,6 @@ const AgentsPage: FC = () => {
</Navbar>
<ContentContainer id="content-container">
<AssistantsContainer>
<Agents onClick={onAddAgentConfirm} />
{tabItems.length > 0 ? (
<Tabs tabPosition="left" animated items={tabItems} />
) : (
@ -155,11 +170,11 @@ const AgentsPage: FC = () => {
)}
</AssistantsContainer>
</ContentContainer>
</Container>
</StyledContainer>
)
}
const Container = styled.div`
const StyledContainer = styled.div`
display: flex;
flex: 1;
flex-direction: column;
@ -248,4 +263,33 @@ 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

@ -1,75 +1,225 @@
import { EllipsisOutlined } from '@ant-design/icons'
import { Agent } from '@renderer/types'
import { Col } from 'antd'
import { Dropdown } from 'antd'
import styled from 'styled-components'
interface Props {
agent: Agent
onClick?: () => void
}
const AgentCard: React.FC<Props> = ({ agent, onClick }) => {
return (
<Container onClick={onClick}>
{agent.emoji && <EmojiHeader>{agent.emoji}</EmojiHeader>}
<Col>
<AgentHeader>
<AgentName style={{ marginBottom: 0 }}>{agent.name}</AgentName>
</AgentHeader>
<AgentCardPrompt className="text-nowrap">
{(agent.description || agent.prompt).substring(0, 20)}
</AgentCardPrompt>
</Col>
</Container>
)
contextMenu?: { label: string; onClick: () => void }[]
menuItems?: {
key: string
label: string
icon?: React.ReactNode
danger?: boolean
onClick: () => void
}[]
}
const Container = styled.div`
width: 100%;
height: 220px;
display: flex;
flex-direction: row;
margin-bottom: 16px;
border: 0.5px solid var(--color-border);
border-radius: 10px;
padding: 15px;
flex-direction: column;
align-items: center;
justify-content: flex-start;
text-align: center;
gap: 10px;
background-color: var(--color-background);
border-radius: 15px;
position: relative;
overflow: hidden;
cursor: pointer;
transition: all 0.2s ease-in-out;
&:hover {
border: 0.5px solid var(--color-primary);
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
border: 0.5px solid var(--color-border);
&::before {
content: '';
width: 100%;
height: 80px;
position: absolute;
top: 0;
left: 0;
border-top-left-radius: 15px;
border-top-right-radius: 15px;
background: var(--color-background-soft);
transition: all 0.5s ease;
border-bottom: none;
}
* {
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%;
}
`
const EmojiHeader = styled.div`
width: 20px;
const EmojiContainer = styled.div`
width: 70px;
height: 70px;
min-width: 70px;
min-height: 70px;
background-color: var(--color-background);
border-radius: 50%;
border: 4px solid var(--color-border);
margin-top: 20px;
transition: all 0.5s ease;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
align-items: center;
margin-right: 5px;
font-size: 24px;
line-height: 20px;
font-size: 32px;
`
const AgentHeader = styled.div`
const CardInfo = styled.div`
display: flex;
flex-direction: row;
justify-content: space-between;
flex-direction: column;
align-items: center;
gap: 8px;
transition: all 0.5s ease;
padding: 0 15px;
width: 100%;
`
const AgentName = styled.div`
line-height: 1.2;
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: 1;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
color: var(--color-text-1);
word-break: break-word;
`
const AgentCardPrompt = styled.div`
color: #666;
margin-top: 6px;
font-size: 12px;
max-width: auto;
const AgentPrompt = styled.p`
color: var(--color-text-soft);
font-size: 14px;
max-width: 100%;
opacity: 0;
transform: translateY(20px);
transition: all 0.5s ease;
margin: 0;
display: -webkit-box;
-webkit-line-clamp: 4;
-webkit-box-orient: vertical;
overflow: hidden;
line-height: 1.4;
`
const BannerBackground = styled.div`
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 80px;
display: flex;
justify-content: center;
align-items: center;
font-size: 500px;
opacity: 0.1;
filter: blur(10px);
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);
width: 24px;
height: 24px;
border-radius: 12px;
font-size: 16px;
color: var(--color-icon);
opacity: 0;
transition: opacity 0.3s;
z-index: 2;
${Container}:hover & {
opacity: 1;
}
`
const AgentCard: React.FC<Props> = ({ agent, onClick, contextMenu, menuItems }) => {
const content = (
<Container onClick={onClick}>
{agent.emoji && <BannerBackground className="banner-background">{agent.emoji}</BannerBackground>}
<EmojiContainer className="emoji-container">{agent.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, 50)}...</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