feat: improvedscrollbar component functionality and added internationalization support to agentspage component
This commit is contained in:
parent
b80270709f
commit
63e5972dd2
@ -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
|
||||||
|
|||||||
@ -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)}
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
180
src/renderer/src/pages/agents/agentGroupTranslations.ts
Normal file
180
src/renderer/src/pages/agents/agentGroupTranslations.ts
Normal 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': '管理'
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -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%;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user