feat(pending-animation): 当消息处于后台pending时,助手头像跟话题显示脉冲动画效果 (#3867)

This commit is contained in:
Teo 2025-03-25 08:48:26 +08:00 committed by GitHub
parent 9b98312775
commit 6699b0902f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 82 additions and 5 deletions

View File

@ -0,0 +1,18 @@
@keyframes pulse {
0% {
box-shadow: 0 0 0 0 rgba(var(--pulse-color), 0.5);
}
70% {
box-shadow: 0 0 0 var(--pulse-size) rgba(var(--pulse-color), 0);
}
100% {
box-shadow: 0 0 0 0 rgba(var(--pulse-color), 0);
}
}
// 电磁波扩散效果
.animation-pulse {
--pulse-color: 59, 130, 246;
--pulse-size: 8px;
animation: pulse 1.5s infinite;
}

View File

@ -2,6 +2,7 @@
@use './ant.scss'; @use './ant.scss';
@use './scrollbar.scss'; @use './scrollbar.scss';
@use './container.scss'; @use './container.scss';
@use './animation.scss';
@import '../fonts/icon-fonts/iconfont.css'; @import '../fonts/icon-fonts/iconfont.css';
@import '../fonts/ubuntu/ubuntu.css'; @import '../fonts/ubuntu/ubuntu.css';

View File

@ -8,9 +8,10 @@ interface Props {
model: Model model: Model
size: number size: number
props?: AvatarProps props?: AvatarProps
className?: string
} }
const ModelAvatar: FC<Props> = ({ model, size, props }) => { const ModelAvatar: FC<Props> = ({ model, size, props, className }) => {
return ( return (
<Avatar <Avatar
src={getModelLogo(model?.id || '')} src={getModelLogo(model?.id || '')}
@ -23,7 +24,8 @@ const ModelAvatar: FC<Props> = ({ model, size, props }) => {
alignItems: 'center', alignItems: 'center',
justifyContent: 'center' justifyContent: 'center'
}} }}
{...props}> {...props}
className={className}>
{first(model?.name)} {first(model?.name)}
</Avatar> </Avatar>
) )

View File

@ -9,10 +9,11 @@ import { getDefaultModel, getDefaultTopic } from '@renderer/services/AssistantSe
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService' import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
import { Assistant } from '@renderer/types' import { Assistant } from '@renderer/types'
import { uuid } from '@renderer/utils' import { uuid } from '@renderer/utils'
import { hasTopicPendingRequests } from '@renderer/utils/queue'
import { Dropdown } from 'antd' import { Dropdown } from 'antd'
import { ItemType } from 'antd/es/menu/interface' import { ItemType } from 'antd/es/menu/interface'
import { omit } from 'lodash' import { omit } from 'lodash'
import { FC, useCallback } from 'react' import { FC, useCallback, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import styled from 'styled-components' import styled from 'styled-components'
@ -32,6 +33,17 @@ const AssistantItem: FC<AssistantItemProps> = ({ assistant, isActive, onSwitch,
const { clickAssistantToShowTopic, topicPosition, showAssistantIcon } = useSettings() const { clickAssistantToShowTopic, topicPosition, showAssistantIcon } = useSettings()
const defaultModel = getDefaultModel() const defaultModel = getDefaultModel()
const [isPending, setIsPending] = useState(false)
useEffect(() => {
if (isActive) {
setIsPending(false)
}
const hasPending = assistant.topics.some((topic) => hasTopicPendingRequests(topic.id))
if (hasPending) {
setIsPending(true)
}
}, [isActive, assistant.topics])
const getMenuItems = useCallback( const getMenuItems = useCallback(
(assistant: Assistant): ItemType[] => [ (assistant: Assistant): ItemType[] => [
{ {
@ -116,7 +128,13 @@ const AssistantItem: FC<AssistantItemProps> = ({ assistant, isActive, onSwitch,
<Dropdown menu={{ items: getMenuItems(assistant) }} trigger={['contextMenu']}> <Dropdown menu={{ items: getMenuItems(assistant) }} trigger={['contextMenu']}>
<Container onClick={handleSwitch} className={isActive ? 'active' : ''}> <Container onClick={handleSwitch} className={isActive ? 'active' : ''}>
<AssistantNameRow className="name" title={fullAssistantName}> <AssistantNameRow className="name" title={fullAssistantName}>
{showAssistantIcon && <ModelAvatar model={assistant.model || defaultModel} size={22} />} {showAssistantIcon && (
<ModelAvatar
model={assistant.model || defaultModel}
size={22}
className={isPending && !isActive ? 'animation-pulse' : ''}
/>
)}
<AssistantName className="text-nowrap">{showAssistantIcon ? assistantName : fullAssistantName}</AssistantName> <AssistantName className="text-nowrap">{showAssistantIcon ? assistantName : fullAssistantName}</AssistantName>
</AssistantNameRow> </AssistantNameRow>
{isActive && ( {isActive && (

View File

@ -32,10 +32,11 @@ import {
exportTopicToNotion, exportTopicToNotion,
topicToMarkdown topicToMarkdown
} from '@renderer/utils/export' } from '@renderer/utils/export'
import { hasTopicPendingRequests } from '@renderer/utils/queue'
import { Dropdown, MenuProps, Tooltip } from 'antd' import { Dropdown, MenuProps, Tooltip } from 'antd'
import dayjs from 'dayjs' import dayjs from 'dayjs'
import { findIndex } from 'lodash' import { findIndex } from 'lodash'
import { FC, useCallback, useRef, useState } from 'react' import { FC, useCallback, useMemo, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import styled from 'styled-components' import styled from 'styled-components'
@ -56,6 +57,28 @@ const Topics: FC<Props> = ({ assistant: _assistant, activeTopic, setActiveTopic
const [deletingTopicId, setDeletingTopicId] = useState<string | null>(null) const [deletingTopicId, setDeletingTopicId] = useState<string | null>(null)
const deleteTimerRef = useRef<NodeJS.Timeout>() const deleteTimerRef = useRef<NodeJS.Timeout>()
const pendingTopics = useMemo(() => {
return new Set<string>()
}, [])
const isPending = useCallback(
(topicId: string) => {
const hasPending = hasTopicPendingRequests(topicId)
if (topicId === activeTopic.id && !hasPending) {
pendingTopics.delete(topicId)
return false
}
if (pendingTopics.has(topicId)) {
return true
}
if (hasPending) {
pendingTopics.add(topicId)
return true
}
return false
},
[activeTopic.id, pendingTopics]
)
const handleDeleteClick = useCallback((topicId: string, e: React.MouseEvent) => { const handleDeleteClick = useCallback((topicId: string, e: React.MouseEvent) => {
e.stopPropagation() e.stopPropagation()
@ -322,6 +345,7 @@ const Topics: FC<Props> = ({ assistant: _assistant, activeTopic, setActiveTopic
className={isActive ? 'active' : ''} className={isActive ? 'active' : ''}
onClick={() => onSwitchTopic(topic)} onClick={() => onSwitchTopic(topic)}
style={{ borderRadius }}> style={{ borderRadius }}>
{isPending(topic.id) && !isActive && <PendingIndicator />}
<TopicName className="name" title={topicName}> <TopicName className="name" title={topicName}>
{topicName} {topicName}
</TopicName> </TopicName>
@ -395,6 +419,7 @@ const TopicListItem = styled.div`
font-family: Ubuntu; font-family: Ubuntu;
cursor: pointer; cursor: pointer;
border: 0.5px solid transparent; border: 0.5px solid transparent;
position: relative;
.menu { .menu {
opacity: 0; opacity: 0;
color: var(--color-text-3); color: var(--color-text-3);
@ -427,6 +452,19 @@ const TopicName = styled.div`
font-size: 13px; font-size: 13px;
` `
const PendingIndicator = styled.div.attrs({
className: 'animation-pulse'
})`
--pulse-size: 5px;
width: 5px;
height: 5px;
position: absolute;
left: 3px;
top: 15px;
border-radius: 50%;
background-color: var(--color-primary);
`
const TopicPromptText = styled.div` const TopicPromptText = styled.div`
color: var(--color-text-2); color: var(--color-text-2);
font-size: 12px; font-size: 12px;