feat(pending-animation): 当消息处于后台pending时,助手头像跟话题显示脉冲动画效果 (#3867)
This commit is contained in:
parent
9b98312775
commit
6699b0902f
18
src/renderer/src/assets/styles/animation.scss
Normal file
18
src/renderer/src/assets/styles/animation.scss
Normal 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;
|
||||||
|
}
|
||||||
@ -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';
|
||||||
|
|
||||||
|
|||||||
@ -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>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -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 && (
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user