From d7b459dcee0778b5c2d161bc06143ff9d5d88bd2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=A6=96=E9=83=BD=E7=88=B1=E6=8A=A4=E5=8A=A8=E7=89=A9?= =?UTF-8?q?=E5=8D=8F=E4=BC=9A?= Date: Thu, 31 Oct 2024 03:25:09 +0800 Subject: [PATCH] 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. --- .../src/components/Scrollbar/index.tsx | 74 ++--- src/renderer/src/pages/agents/Agents.tsx | 287 ++++++++++-------- src/renderer/src/pages/agents/AgentsPage.tsx | 74 ++++- .../src/pages/agents/components/AgentCard.tsx | 240 ++++++++++++--- 4 files changed, 446 insertions(+), 229 deletions(-) diff --git a/src/renderer/src/components/Scrollbar/index.tsx b/src/renderer/src/components/Scrollbar/index.tsx index b601fce0bb..c367b80d66 100644 --- a/src/renderer/src/components/Scrollbar/index.tsx +++ b/src/renderer/src/components/Scrollbar/index.tsx @@ -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 { - right?: boolean - ref?: any +interface Props { + children?: React.ReactNode + className?: string + $isScrolling?: boolean + $right?: boolean } -const Scrollbar: FC = forwardRef((props, ref) => { - const [isScrolling, setIsScrolling] = useState(false) - const timeoutRef = useRef(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(({ children, className, $isScrolling, $right }, ref) => { return ( - - {props.children} - + + {children} + ) }) 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 diff --git a/src/renderer/src/pages/agents/Agents.tsx b/src/renderer/src/pages/agents/Agents.tsx index 9df144495f..331e82d1f6 100644 --- a/src/renderer/src/pages/agents/Agents.tsx +++ b/src/renderer/src/pages/agents/Agents.tsx @@ -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 = ({ onClick }) => { +const Agents: React.FC = ({ 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: , - onClick: () => AssistantSettingsPopup.show({ assistant: agent }) - }, - { - label: t('agents.add.button'), - key: 'create', - icon: , - onClick: () => createAssistantFromAgent(agent) - }, - { type: 'divider' }, - { - label: t('common.delete'), - key: 'delete', - icon: , - 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: , + onClick: () => AssistantSettingsPopup.show({ assistant: agent }) + }, + { + key: 'create', + label: t('agents.add.button'), + icon: , + onClick: () => createAssistantFromAgent(agent) + }, + { + key: 'delete', + label: t('common.delete'), + icon: , + 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 ( + + onClick?.(agent)} + contextMenu={contextMenuItems} + menuItems={dropdownMenuItems} + /> + + ) + })} + + ) + } + return ( - - - {t('agents.my_agents')} - - {agents.length > 0 && ( - setDragging(true)} - onDragEnd={() => setDragging(false)}> - {(agent: Agent) => ( - - onClick(agent)}> - - - {agent.emoji} {agent.name} - - e.stopPropagation()}> - - - - - - {agent.prompt} - - - )} - - )} - {!dragging && ( - - )} -
+ +
+ + {t('agents.my_agents')} + + {agents.length > 0 && ( + setDragging(true)} + onDragEnd={() => setDragging(false)}> + {(agent: Agent) => { + const dropdownMenuItems = [ + { + key: 'edit', + label: t('agents.edit.title'), + icon: , + onClick: () => AssistantSettingsPopup.show({ assistant: agent }) + }, + { + key: 'create', + label: t('agents.add.button'), + icon: , + onClick: () => createAssistantFromAgent(agent) + }, + { + key: 'delete', + label: t('common.delete'), + icon: , + 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 ( + onClick?.(agent)} + contextMenu={contextMenuItems} + menuItems={dropdownMenuItems} + /> + ) + }} + + )} + {!dragging && ( + + )} +
+
) } -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 diff --git a/src/renderer/src/pages/agents/AgentsPage.tsx b/src/renderer/src/pages/agents/AgentsPage.tsx index f659b58c59..96a756b5d0 100644 --- a/src/renderer/src/pages/agents/AgentsPage.tsx +++ b/src/renderer/src/pages/agents/AgentsPage.tsx @@ -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 = () => { {group} - - {filteredAgentGroups[group].map((agent, index) => { - return ( + + {group === '我的' ? ( + <> + + AddAgentPopup.show()} /> + + + + ) : ( + filteredAgentGroups[group]?.map((agent, index) => ( onAddAgentConfirm(getAgentFromSystemAgent(agent))} agent={agent as any} /> - ) - })} + )) + )} ) @@ -124,7 +140,7 @@ const AgentsPage: FC = () => { }, [filteredAgentGroups, onAddAgentConfirm]) return ( - + {t('agents.title')} @@ -145,7 +161,6 @@ const AgentsPage: FC = () => { - {tabItems.length > 0 ? ( ) : ( @@ -155,11 +170,11 @@ const AgentsPage: FC = () => { )} - + ) } -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 ( +
+ + {t('agents.add.title')} +
+ ) +})` + 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 diff --git a/src/renderer/src/pages/agents/components/AgentCard.tsx b/src/renderer/src/pages/agents/components/AgentCard.tsx index 268e034c0f..433ecaf82f 100644 --- a/src/renderer/src/pages/agents/components/AgentCard.tsx +++ b/src/renderer/src/pages/agents/components/AgentCard.tsx @@ -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 = ({ agent, onClick }) => { - return ( - - {agent.emoji && {agent.emoji}} - - - {agent.name} - - - {(agent.description || agent.prompt).substring(0, 20)} - - - - ) + 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 = ({ agent, onClick, contextMenu, menuItems }) => { + const content = ( + + {agent.emoji && {agent.emoji}} + {agent.emoji} + {menuItems && ( + e.stopPropagation()}> + ({ + ...item, + onClick: (e) => { + e.domEvent.stopPropagation() + e.domEvent.preventDefault() + setTimeout(() => { + item.onClick() + }, 0) + } + })) + }} + trigger={['click']} + placement="bottomRight"> + + + + )} + + {agent.name} + {(agent.description || agent.prompt).substring(0, 50)}... + + + ) + + if (contextMenu) { + return ( + ({ + key: item.label, + label: item.label, + onClick: () => item.onClick() + })) + }} + trigger={['contextMenu']}> + {content} + + ) + } + + return content +} + export default AgentCard