feat: improvedscrollbar component functionality and added internationalization support to agentspage component

This commit is contained in:
kangfenmao 2024-10-31 11:31:38 +08:00
parent b80270709f
commit 63e5972dd2
6 changed files with 248 additions and 45 deletions

View File

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

View File

@ -75,7 +75,7 @@ const Agents: React.FC<Props> = ({ onClick, cardStyle = 'old' }) => {
] ]
return ( return (
<Col span={8} key={agent.id}> <Col span={8} xxl={6} key={agent.id}>
<AgentCard <AgentCard
agent={agent} agent={agent}
onClick={() => onClick?.(agent)} onClick={() => onClick?.(agent)}

View File

@ -12,6 +12,7 @@ import { useTranslation } from 'react-i18next'
import ReactMarkdown from 'react-markdown' import ReactMarkdown from 'react-markdown'
import styled from 'styled-components' import styled from 'styled-components'
import { groupTranslations } from './agentGroupTranslations'
import Agents from './Agents' import Agents from './Agents'
import AddAgentPopup from './components/AddAgentPopup' import AddAgentPopup from './components/AddAgentPopup'
import AgentCard from './components/AgentCard' import AgentCard from './components/AgentCard'
@ -41,7 +42,7 @@ const AgentsPage: FC = () => {
return _agentGroups return _agentGroups
}, []) }, [])
const { t } = useTranslation() const { t, i18n } = useTranslation()
const filteredAgentGroups = useMemo(() => { const filteredAgentGroups = useMemo(() => {
const groups = search.trim() ? {} : { : [] } const groups = search.trim() ? {} : { : [] }
@ -102,6 +103,14 @@ const AgentsPage: FC = () => {
} }
} }
const getLocalizedGroupName = useCallback(
(group: string) => {
const currentLang = i18n.language
return groupTranslations[group]?.[currentLang] || group
},
[i18n.language]
)
const tabItems = useMemo(() => { const tabItems = useMemo(() => {
let groups = Object.keys(filteredAgentGroups) let groups = Object.keys(filteredAgentGroups)
groups = groups.filter((g) => g !== '我的' && g !== '办公') groups = groups.filter((g) => g !== '我的' && g !== '办公')
@ -109,25 +118,27 @@ const AgentsPage: FC = () => {
return groups.map((group, i) => { return groups.map((group, i) => {
const id = String(i + 1) const id = String(i + 1)
const localizedGroupName = getLocalizedGroupName(group)
return { return {
label: group, label: localizedGroupName,
key: id, key: id,
children: ( children: (
<TabContent key={group}> <TabContent key={group}>
<Title level={5} key={group} style={{ marginBottom: 16 }}> <Title level={5} key={group} style={{ marginBottom: 16 }}>
{group} {localizedGroupName}
</Title> </Title>
<Row gutter={[32, 32]}> <Row gutter={[25, 25]}>
{group === '我的' ? ( {group === '我的' ? (
<> <>
<Col span={8}> <Col span={8} xxl={6}>
<AddAgentCard onClick={() => AddAgentPopup.show()} /> <AddAgentCard onClick={() => AddAgentPopup.show()} />
</Col> </Col>
<Agents onClick={onAddAgentConfirm} cardStyle="new" /> <Agents onClick={onAddAgentConfirm} cardStyle="new" />
</> </>
) : ( ) : (
filteredAgentGroups[group]?.map((agent, index) => ( filteredAgentGroups[group]?.map((agent, index) => (
<Col span={8} key={group + index}> <Col span={8} xxl={6} key={group + index}>
<AgentCard onClick={() => onAddAgentConfirm(getAgentFromSystemAgent(agent))} agent={agent as any} /> <AgentCard onClick={() => onAddAgentConfirm(getAgentFromSystemAgent(agent))} agent={agent as any} />
</Col> </Col>
)) ))
@ -137,7 +148,7 @@ const AgentsPage: FC = () => {
) )
} }
}) })
}, [filteredAgentGroups, onAddAgentConfirm]) }, [filteredAgentGroups, getLocalizedGroupName, onAddAgentConfirm])
return ( return (
<StyledContainer> <StyledContainer>
@ -187,6 +198,7 @@ const ContentContainer = styled.div`
flex-direction: row; flex-direction: row;
justify-content: center; justify-content: center;
height: 100%; height: 100%;
padding: 0 10px;
` `
const AssistantsContainer = styled.div` const AssistantsContainer = styled.div`
@ -200,6 +212,7 @@ 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;
` `
const AgentPrompt = styled.div` const AgentPrompt = styled.div`
@ -215,7 +228,6 @@ const EmptyView = styled.div`
align-items: center; align-items: center;
font-size: 16px; font-size: 16px;
color: var(--color-text-secondary); color: var(--color-text-secondary);
border-left: 0.5px solid var(--color-border);
` `
const Tabs = styled(TabsAntd)` const Tabs = styled(TabsAntd)`
@ -247,7 +259,7 @@ const Tabs = styled(TabsAntd)`
border-right: none; border-right: none;
} }
.ant-tabs-content-holder { .ant-tabs-content-holder {
border-left: 0.5px solid var(--color-border); border-left: none;
border-right: 0.5px solid var(--color-border); border-right: 0.5px solid var(--color-border);
} }
.ant-tabs-ink-bar { .ant-tabs-ink-bar {

View File

@ -0,0 +1,180 @@
export type GroupTranslations = {
[key: string]: {
'en-US': string
'zh-CN': string
'zh-TW': string
}
}
export const groupTranslations: GroupTranslations = {
: {
'en-US': 'My Agents',
'zh-CN': '我的',
'zh-TW': '我的'
},
: {
'en-US': 'Career',
'zh-CN': '职业',
'zh-TW': '職業'
},
: {
'en-US': 'Business',
'zh-CN': '商业',
'zh-TW': '商業'
},
: {
'en-US': 'Tools',
'zh-CN': '工具',
'zh-TW': '工具'
},
: {
'en-US': 'Language',
'zh-CN': '语言',
'zh-TW': '語言'
},
: {
'en-US': 'Office',
'zh-CN': '办公',
'zh-TW': '辦公'
},
: {
'en-US': 'General',
'zh-CN': '通用',
'zh-TW': '通用'
},
: {
'en-US': 'Writing',
'zh-CN': '写作',
'zh-TW': '寫作'
},
Artifacts: {
'en-US': 'Artifacts',
'zh-CN': 'Artifacts',
'zh-TW': 'Artifacts'
},
: {
'en-US': 'Programming',
'zh-CN': '编程',
'zh-TW': '編程'
},
: {
'en-US': 'Emotion',
'zh-CN': '情感',
'zh-TW': '情感'
},
: {
'en-US': 'Education',
'zh-CN': '教育',
'zh-TW': '教育'
},
: {
'en-US': 'Creative',
'zh-CN': '创意',
'zh-TW': '創意'
},
: {
'en-US': 'Academic',
'zh-CN': '学术',
'zh-TW': '學術'
},
: {
'en-US': 'Design',
'zh-CN': '设计',
'zh-TW': '設計'
},
: {
'en-US': 'Art',
'zh-CN': '艺术',
'zh-TW': '藝術'
},
: {
'en-US': 'Entertainment',
'zh-CN': '娱乐',
'zh-TW': '娛樂'
},
: {
'en-US': 'Life',
'zh-CN': '生活',
'zh-TW': '生活'
},
: {
'en-US': 'Medical',
'zh-CN': '医疗',
'zh-TW': '醫療'
},
: {
'en-US': 'Games',
'zh-CN': '游戏',
'zh-TW': '遊戲'
},
: {
'en-US': 'Translation',
'zh-CN': '翻译',
'zh-TW': '翻譯'
},
: {
'en-US': 'Music',
'zh-CN': '音乐',
'zh-TW': '音樂'
},
: {
'en-US': 'Review',
'zh-CN': '点评',
'zh-TW': '點評'
},
: {
'en-US': 'Copywriting',
'zh-CN': '文案',
'zh-TW': '文案'
},
: {
'en-US': 'Encyclopedia',
'zh-CN': '百科',
'zh-TW': '百科'
},
: {
'en-US': 'Health',
'zh-CN': '健康',
'zh-TW': '健康'
},
: {
'en-US': 'Marketing',
'zh-CN': '营销',
'zh-TW': '營銷'
},
: {
'en-US': 'Science',
'zh-CN': '科学',
'zh-TW': '科學'
},
: {
'en-US': 'Analysis',
'zh-CN': '分析',
'zh-TW': '分析'
},
: {
'en-US': 'Legal',
'zh-CN': '法律',
'zh-TW': '法律'
},
: {
'en-US': 'Consulting',
'zh-CN': '咨询',
'zh-TW': '諮詢'
},
: {
'en-US': 'Finance',
'zh-CN': '金融',
'zh-TW': '金融'
},
: {
'en-US': 'Travel',
'zh-CN': '旅游',
'zh-TW': '旅遊'
},
: {
'en-US': 'Management',
'zh-CN': '管理',
'zh-TW': '管理'
}
}

View File

@ -1,5 +1,6 @@
import { EllipsisOutlined } from '@ant-design/icons' import { EllipsisOutlined } from '@ant-design/icons'
import { Agent } from '@renderer/types' import { Agent } from '@renderer/types'
import { getLeadingEmoji } from '@renderer/utils'
import { Dropdown } from 'antd' import { Dropdown } from 'antd'
import styled from 'styled-components' import styled from 'styled-components'
@ -171,10 +172,11 @@ const MenuContainer = styled.div`
` `
const AgentCard: React.FC<Props> = ({ agent, onClick, contextMenu, menuItems }) => { const AgentCard: React.FC<Props> = ({ agent, onClick, contextMenu, menuItems }) => {
const emoji = agent.emoji || getLeadingEmoji(agent.name)
const content = ( const content = (
<Container onClick={onClick}> <Container onClick={onClick}>
{agent.emoji && <BannerBackground className="banner-background">{agent.emoji}</BannerBackground>} {agent.emoji && <BannerBackground className="banner-background">{agent.emoji}</BannerBackground>}
<EmojiContainer className="emoji-container">{agent.emoji}</EmojiContainer> <EmojiContainer className="emoji-container">{emoji}</EmojiContainer>
{menuItems && ( {menuItems && (
<MenuContainer onClick={(e) => e.stopPropagation()}> <MenuContainer onClick={(e) => e.stopPropagation()}>
<Dropdown <Dropdown
@ -198,7 +200,7 @@ const AgentCard: React.FC<Props> = ({ agent, onClick, contextMenu, menuItems })
)} )}
<CardInfo className="card-info"> <CardInfo className="card-info">
<AgentName>{agent.name}</AgentName> <AgentName>{agent.name}</AgentName>
<AgentPrompt className="agent-prompt">{(agent.description || agent.prompt).substring(0, 50)}...</AgentPrompt> <AgentPrompt className="agent-prompt">{(agent.description || agent.prompt).substring(0, 100)}...</AgentPrompt>
</CardInfo> </CardInfo>
</Container> </Container>
) )

View File

@ -153,6 +153,7 @@ const ImageWrapper = styled.div`
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
border: 0.5px solid var(--color-border);
.ant-image { .ant-image {
height: 100%; height: 100%;