mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2026-01-01 01:30:51 +08:00
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:
parent
76b9e1a65e
commit
d7b459dcee
@ -1,57 +1,49 @@
|
|||||||
import { throttle } from 'lodash'
|
import { forwardRef } from 'react'
|
||||||
import { FC, forwardRef, useCallback, useEffect, useRef, useState } from 'react'
|
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
|
|
||||||
interface Props extends React.HTMLAttributes<HTMLDivElement> {
|
interface Props {
|
||||||
right?: boolean
|
children?: React.ReactNode
|
||||||
ref?: any
|
className?: string
|
||||||
|
$isScrolling?: boolean
|
||||||
|
$right?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const Scrollbar: FC<Props> = forwardRef<HTMLDivElement, Props>((props, ref) => {
|
const ScrollbarContainer = styled.div<{ $isScrolling?: boolean; $right?: boolean }>`
|
||||||
const [isScrolling, setIsScrolling] = useState(false)
|
overflow-y: auto;
|
||||||
const timeoutRef = useRef<NodeJS.Timeout | null>(null)
|
overflow-x: hidden;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
const handleScroll = useCallback(
|
&::-webkit-scrollbar {
|
||||||
throttle(() => {
|
width: 6px;
|
||||||
setIsScrolling(true)
|
height: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
if (timeoutRef.current) {
|
&::-webkit-scrollbar-track {
|
||||||
clearTimeout(timeoutRef.current)
|
border-radius: 3px;
|
||||||
}
|
background: transparent;
|
||||||
|
${({ $right }) => $right && `margin-right: 4px;`}
|
||||||
|
}
|
||||||
|
|
||||||
timeoutRef.current = setTimeout(() => setIsScrolling(false), 1500) // 增加到 2 秒
|
&::-webkit-scrollbar-thumb {
|
||||||
}, 200),
|
border-radius: 3px;
|
||||||
[]
|
background: ${({ $isScrolling }) =>
|
||||||
)
|
$isScrolling ? 'var(--color-scrollbar-thumb)' : 'var(--color-scrollbar-track)'};
|
||||||
|
transition: all 0.2s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
&:hover::-webkit-scrollbar-thumb {
|
||||||
return () => {
|
background: var(--color-scrollbar-thumb);
|
||||||
if (timeoutRef.current) {
|
}
|
||||||
clearTimeout(timeoutRef.current)
|
`
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
|
const Scrollbar = forwardRef<HTMLDivElement, Props>(({ children, className, $isScrolling, $right }, ref) => {
|
||||||
return (
|
return (
|
||||||
<Container {...props} isScrolling={isScrolling} onScroll={handleScroll} ref={ref}>
|
<ScrollbarContainer ref={ref} className={className} $isScrolling={$isScrolling} $right={$right}>
|
||||||
{props.children}
|
{children}
|
||||||
</Container>
|
</ScrollbarContainer>
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
Scrollbar.displayName = 'Scrollbar'
|
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
|
export default Scrollbar
|
||||||
|
|||||||
@ -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 AssistantSettingsPopup from '@renderer/components/AssistantSettings'
|
||||||
import DragableList from '@renderer/components/DragableList'
|
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 { 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, Dropdown, Typography } from 'antd'
|
import { Button, Col, Typography } from 'antd'
|
||||||
import { ItemType } from 'antd/es/menu/interface'
|
|
||||||
import { useCallback, useState } from 'react'
|
import { useCallback, useState } 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 AddAgentPopup from './components/AddAgentPopup'
|
||||||
|
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 }) => {
|
const Agents: React.FC<Props> = ({ onClick, cardStyle = 'old' }) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { agents, removeAgent, updateAgents } = useAgents()
|
const { agents, removeAgent, updateAgents } = useAgents()
|
||||||
const [dragging, setDragging] = useState(false)
|
const [dragging, setDragging] = useState(false)
|
||||||
|
|
||||||
const getMenuItems = useCallback(
|
const handleDelete = useCallback(
|
||||||
(agent: Agent) =>
|
(agent: Agent) => {
|
||||||
[
|
window.modal.confirm({
|
||||||
{
|
centered: true,
|
||||||
label: t('agents.edit.title'),
|
content: t('agents.delete.popup.content'),
|
||||||
key: 'edit',
|
onOk: () => removeAgent(agent.id)
|
||||||
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[],
|
|
||||||
[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} key={agent.id}>
|
||||||
|
<AgentCard
|
||||||
|
agent={agent}
|
||||||
|
onClick={() => onClick?.(agent)}
|
||||||
|
contextMenu={contextMenuItems}
|
||||||
|
menuItems={dropdownMenuItems}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container style={{ paddingBottom: dragging ? 30 : 0 }}>
|
<Container>
|
||||||
<Typography.Title level={5} style={{ marginBottom: 16 }}>
|
<div style={{ paddingBottom: dragging ? 30 : 0 }}>
|
||||||
{t('agents.my_agents')}
|
<Typography.Title level={5} style={{ marginBottom: 16 }}>
|
||||||
</Typography.Title>
|
{t('agents.my_agents')}
|
||||||
{agents.length > 0 && (
|
</Typography.Title>
|
||||||
<DragableList
|
{agents.length > 0 && (
|
||||||
list={agents}
|
<DragableList
|
||||||
onUpdate={updateAgents}
|
list={agents}
|
||||||
onDragStart={() => setDragging(true)}
|
onUpdate={updateAgents}
|
||||||
onDragEnd={() => setDragging(false)}>
|
onDragStart={() => setDragging(true)}
|
||||||
{(agent: Agent) => (
|
onDragEnd={() => setDragging(false)}>
|
||||||
<Dropdown menu={{ items: getMenuItems(agent) }} trigger={['contextMenu']}>
|
{(agent: Agent) => {
|
||||||
<AgentItem onClick={() => onClick(agent)}>
|
const dropdownMenuItems = [
|
||||||
<HStack alignItems="center" justifyContent="space-between" h="36px">
|
{
|
||||||
<AgentItemName className="text-nowrap">
|
key: 'edit',
|
||||||
{agent.emoji} {agent.name}
|
label: t('agents.edit.title'),
|
||||||
</AgentItemName>
|
icon: <EditOutlined />,
|
||||||
<ActionButton className="actions" gap="15px" onClick={(e) => e.stopPropagation()}>
|
onClick: () => AssistantSettingsPopup.show({ assistant: agent })
|
||||||
<Dropdown menu={{ items: getMenuItems(agent) }} trigger={['hover']}>
|
},
|
||||||
<MoreOutlined style={{ cursor: 'pointer' }} />
|
{
|
||||||
</Dropdown>
|
key: 'create',
|
||||||
</ActionButton>
|
label: t('agents.add.button'),
|
||||||
</HStack>
|
icon: <PlusOutlined />,
|
||||||
<AgentItemPrompt>{agent.prompt}</AgentItemPrompt>
|
onClick: () => createAssistantFromAgent(agent)
|
||||||
</AgentItem>
|
},
|
||||||
</Dropdown>
|
{
|
||||||
)}
|
key: 'delete',
|
||||||
</DragableList>
|
label: t('common.delete'),
|
||||||
)}
|
icon: <DeleteOutlined />,
|
||||||
{!dragging && (
|
danger: true,
|
||||||
<Button
|
onClick: () => handleDelete(agent)
|
||||||
type="dashed"
|
}
|
||||||
icon={<PlusOutlined />}
|
]
|
||||||
onClick={() => AddAgentPopup.show()}
|
|
||||||
style={{ borderRadius: 20, height: 34 }}>
|
const contextMenuItems = [
|
||||||
{t('agents.add.title')}
|
{
|
||||||
</Button>
|
label: t('agents.edit.title'),
|
||||||
)}
|
onClick: () => AssistantSettingsPopup.show({ assistant: agent })
|
||||||
<div style={{ height: 10 }} />
|
},
|
||||||
|
{
|
||||||
|
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>
|
</Container>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const Container = styled(Scrollbar)`
|
const Container = styled.div`
|
||||||
padding: 10px 15px;
|
padding: 10px 15px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
min-height: calc(100vh - var(--navbar-height));
|
min-height: calc(100vh - var(--navbar-height));
|
||||||
min-width: var(--assistants-width);
|
min-width: var(--assistants-width);
|
||||||
max-width: var(--assistants-width);
|
max-width: var(--assistants-width);
|
||||||
`
|
overflow-y: auto;
|
||||||
|
overflow-x: hidden;
|
||||||
|
|
||||||
const AgentItem = styled.div`
|
&::-webkit-scrollbar {
|
||||||
display: flex;
|
width: 6px;
|
||||||
flex-direction: column;
|
height: 6px;
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
&:hover {
|
|
||||||
border: 0.5px solid var(--color-primary);
|
&::-webkit-scrollbar-track {
|
||||||
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
|
border-radius: 3px;
|
||||||
|
background: transparent;
|
||||||
}
|
}
|
||||||
`
|
|
||||||
|
|
||||||
const AgentItemName = styled.div`
|
&::-webkit-scrollbar-thumb {
|
||||||
display: flex;
|
border-radius: 3px;
|
||||||
flex-direction: row;
|
background: var(--color-scrollbar-thumb);
|
||||||
align-items: center;
|
transition: all 0.2s ease-in-out;
|
||||||
`
|
}
|
||||||
|
|
||||||
const AgentItemPrompt = styled.div`
|
&:hover::-webkit-scrollbar-thumb {
|
||||||
font-size: 12px;
|
background: var(--color-scrollbar-thumb);
|
||||||
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);
|
|
||||||
`
|
`
|
||||||
|
|
||||||
export default Agents
|
export default Agents
|
||||||
|
|||||||
@ -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 { 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'
|
||||||
@ -13,6 +13,7 @@ import ReactMarkdown from 'react-markdown'
|
|||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
|
|
||||||
import Agents from './Agents'
|
import Agents from './Agents'
|
||||||
|
import AddAgentPopup from './components/AddAgentPopup'
|
||||||
import AgentCard from './components/AgentCard'
|
import AgentCard from './components/AgentCard'
|
||||||
|
|
||||||
const { Title } = Typography
|
const { Title } = Typography
|
||||||
@ -43,9 +44,15 @@ const AgentsPage: FC = () => {
|
|||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
|
||||||
const filteredAgentGroups = useMemo(() => {
|
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]) => {
|
Object.entries(agentGroups).forEach(([group, agents]) => {
|
||||||
const filteredAgents = agents.filter(
|
const filteredAgents = agents.filter(
|
||||||
(agent) =>
|
(agent) =>
|
||||||
@ -53,10 +60,10 @@ const AgentsPage: FC = () => {
|
|||||||
agent.description?.toLowerCase().includes(search.toLowerCase())
|
agent.description?.toLowerCase().includes(search.toLowerCase())
|
||||||
)
|
)
|
||||||
if (filteredAgents.length > 0) {
|
if (filteredAgents.length > 0) {
|
||||||
filtered[group] = filteredAgents
|
groups[group] = filteredAgents
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
return filtered
|
return groups
|
||||||
}, [agentGroups, search])
|
}, [agentGroups, search])
|
||||||
|
|
||||||
const getAgentName = (agent: Agent) => {
|
const getAgentName = (agent: Agent) => {
|
||||||
@ -97,7 +104,9 @@ const AgentsPage: FC = () => {
|
|||||||
|
|
||||||
const tabItems = useMemo(() => {
|
const tabItems = useMemo(() => {
|
||||||
let groups = Object.keys(filteredAgentGroups)
|
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) => {
|
return groups.map((group, i) => {
|
||||||
const id = String(i + 1)
|
const id = String(i + 1)
|
||||||
return {
|
return {
|
||||||
@ -108,14 +117,21 @@ const AgentsPage: FC = () => {
|
|||||||
<Title level={5} key={group} style={{ marginBottom: 16 }}>
|
<Title level={5} key={group} style={{ marginBottom: 16 }}>
|
||||||
{group}
|
{group}
|
||||||
</Title>
|
</Title>
|
||||||
<Row gutter={16}>
|
<Row gutter={[32, 32]}>
|
||||||
{filteredAgentGroups[group].map((agent, index) => {
|
{group === '我的' ? (
|
||||||
return (
|
<>
|
||||||
|
<Col span={8}>
|
||||||
|
<AddAgentCard onClick={() => AddAgentPopup.show()} />
|
||||||
|
</Col>
|
||||||
|
<Agents onClick={onAddAgentConfirm} cardStyle="new" />
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
filteredAgentGroups[group]?.map((agent, index) => (
|
||||||
<Col span={8} key={group + index}>
|
<Col span={8} key={group + index}>
|
||||||
<AgentCard onClick={() => onAddAgentConfirm(getAgentFromSystemAgent(agent))} agent={agent as any} />
|
<AgentCard onClick={() => onAddAgentConfirm(getAgentFromSystemAgent(agent))} agent={agent as any} />
|
||||||
</Col>
|
</Col>
|
||||||
)
|
))
|
||||||
})}
|
)}
|
||||||
</Row>
|
</Row>
|
||||||
</TabContent>
|
</TabContent>
|
||||||
)
|
)
|
||||||
@ -124,7 +140,7 @@ const AgentsPage: FC = () => {
|
|||||||
}, [filteredAgentGroups, onAddAgentConfirm])
|
}, [filteredAgentGroups, onAddAgentConfirm])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container>
|
<StyledContainer>
|
||||||
<Navbar>
|
<Navbar>
|
||||||
<NavbarCenter style={{ borderRight: 'none', justifyContent: 'space-between' }}>
|
<NavbarCenter style={{ borderRight: 'none', justifyContent: 'space-between' }}>
|
||||||
{t('agents.title')}
|
{t('agents.title')}
|
||||||
@ -145,7 +161,6 @@ const AgentsPage: FC = () => {
|
|||||||
</Navbar>
|
</Navbar>
|
||||||
<ContentContainer id="content-container">
|
<ContentContainer id="content-container">
|
||||||
<AssistantsContainer>
|
<AssistantsContainer>
|
||||||
<Agents onClick={onAddAgentConfirm} />
|
|
||||||
{tabItems.length > 0 ? (
|
{tabItems.length > 0 ? (
|
||||||
<Tabs tabPosition="left" animated items={tabItems} />
|
<Tabs tabPosition="left" animated items={tabItems} />
|
||||||
) : (
|
) : (
|
||||||
@ -155,11 +170,11 @@ const AgentsPage: FC = () => {
|
|||||||
)}
|
)}
|
||||||
</AssistantsContainer>
|
</AssistantsContainer>
|
||||||
</ContentContainer>
|
</ContentContainer>
|
||||||
</Container>
|
</StyledContainer>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const Container = styled.div`
|
const StyledContainer = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
flex-direction: column;
|
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
|
export default AgentsPage
|
||||||
|
|||||||
@ -1,75 +1,225 @@
|
|||||||
|
import { EllipsisOutlined } from '@ant-design/icons'
|
||||||
import { Agent } from '@renderer/types'
|
import { Agent } from '@renderer/types'
|
||||||
import { Col } from 'antd'
|
import { Dropdown } from 'antd'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
agent: Agent
|
agent: Agent
|
||||||
onClick?: () => void
|
onClick?: () => void
|
||||||
}
|
contextMenu?: { label: string; onClick: () => void }[]
|
||||||
|
menuItems?: {
|
||||||
const AgentCard: React.FC<Props> = ({ agent, onClick }) => {
|
key: string
|
||||||
return (
|
label: string
|
||||||
<Container onClick={onClick}>
|
icon?: React.ReactNode
|
||||||
{agent.emoji && <EmojiHeader>{agent.emoji}</EmojiHeader>}
|
danger?: boolean
|
||||||
<Col>
|
onClick: () => void
|
||||||
<AgentHeader>
|
}[]
|
||||||
<AgentName style={{ marginBottom: 0 }}>{agent.name}</AgentName>
|
|
||||||
</AgentHeader>
|
|
||||||
<AgentCardPrompt className="text-nowrap">
|
|
||||||
{(agent.description || agent.prompt).substring(0, 20)}
|
|
||||||
</AgentCardPrompt>
|
|
||||||
</Col>
|
|
||||||
</Container>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const Container = styled.div`
|
const Container = styled.div`
|
||||||
|
width: 100%;
|
||||||
|
height: 220px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: column;
|
||||||
margin-bottom: 16px;
|
align-items: center;
|
||||||
border: 0.5px solid var(--color-border);
|
justify-content: flex-start;
|
||||||
border-radius: 10px;
|
text-align: center;
|
||||||
padding: 15px;
|
gap: 10px;
|
||||||
|
background-color: var(--color-background);
|
||||||
|
border-radius: 15px;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.2s ease-in-out;
|
border: 0.5px solid var(--color-border);
|
||||||
&:hover {
|
|
||||||
border: 0.5px solid var(--color-primary);
|
&::before {
|
||||||
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
|
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;
|
display: flex;
|
||||||
flex-direction: row;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
font-size: 32px;
|
||||||
margin-right: 5px;
|
|
||||||
font-size: 24px;
|
|
||||||
line-height: 20px;
|
|
||||||
`
|
`
|
||||||
|
|
||||||
const AgentHeader = styled.div`
|
const CardInfo = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: column;
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
transition: all 0.5s ease;
|
||||||
|
padding: 0 15px;
|
||||||
|
width: 100%;
|
||||||
`
|
`
|
||||||
|
|
||||||
const AgentName = styled.div`
|
const AgentName = styled.span`
|
||||||
line-height: 1.2;
|
font-weight: 600;
|
||||||
|
font-size: 16px;
|
||||||
|
color: var(--color-text);
|
||||||
|
margin-top: 5px;
|
||||||
|
line-height: 1.4;
|
||||||
|
max-width: 100%;
|
||||||
display: -webkit-box;
|
display: -webkit-box;
|
||||||
-webkit-line-clamp: 1;
|
-webkit-line-clamp: 2;
|
||||||
-webkit-box-orient: vertical;
|
-webkit-box-orient: vertical;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
color: var(--color-text-1);
|
word-break: break-word;
|
||||||
`
|
`
|
||||||
|
|
||||||
const AgentCardPrompt = styled.div`
|
const AgentPrompt = styled.p`
|
||||||
color: #666;
|
color: var(--color-text-soft);
|
||||||
margin-top: 6px;
|
font-size: 14px;
|
||||||
font-size: 12px;
|
max-width: 100%;
|
||||||
max-width: auto;
|
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
|
export default AgentCard
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user