diff --git a/src/renderer/src/assets/styles/animation.scss b/src/renderer/src/assets/styles/animation.scss new file mode 100644 index 00000000..5d02acfc --- /dev/null +++ b/src/renderer/src/assets/styles/animation.scss @@ -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; +} diff --git a/src/renderer/src/assets/styles/index.scss b/src/renderer/src/assets/styles/index.scss index 5531ec5a..2151d8c7 100644 --- a/src/renderer/src/assets/styles/index.scss +++ b/src/renderer/src/assets/styles/index.scss @@ -2,6 +2,7 @@ @use './ant.scss'; @use './scrollbar.scss'; @use './container.scss'; +@use './animation.scss'; @import '../fonts/icon-fonts/iconfont.css'; @import '../fonts/ubuntu/ubuntu.css'; diff --git a/src/renderer/src/components/Avatar/ModelAvatar.tsx b/src/renderer/src/components/Avatar/ModelAvatar.tsx index b7d06d88..d1a6f98b 100644 --- a/src/renderer/src/components/Avatar/ModelAvatar.tsx +++ b/src/renderer/src/components/Avatar/ModelAvatar.tsx @@ -8,9 +8,10 @@ interface Props { model: Model size: number props?: AvatarProps + className?: string } -const ModelAvatar: FC = ({ model, size, props }) => { +const ModelAvatar: FC = ({ model, size, props, className }) => { return ( = ({ model, size, props }) => { alignItems: 'center', justifyContent: 'center' }} - {...props}> + {...props} + className={className}> {first(model?.name)} ) diff --git a/src/renderer/src/pages/home/Tabs/AssistantItem.tsx b/src/renderer/src/pages/home/Tabs/AssistantItem.tsx index e7e5b8dd..ec4f32f9 100644 --- a/src/renderer/src/pages/home/Tabs/AssistantItem.tsx +++ b/src/renderer/src/pages/home/Tabs/AssistantItem.tsx @@ -9,10 +9,11 @@ import { getDefaultModel, getDefaultTopic } from '@renderer/services/AssistantSe import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService' import { Assistant } from '@renderer/types' import { uuid } from '@renderer/utils' +import { hasTopicPendingRequests } from '@renderer/utils/queue' import { Dropdown } from 'antd' import { ItemType } from 'antd/es/menu/interface' import { omit } from 'lodash' -import { FC, useCallback } from 'react' +import { FC, useCallback, useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' import styled from 'styled-components' @@ -32,6 +33,17 @@ const AssistantItem: FC = ({ assistant, isActive, onSwitch, const { clickAssistantToShowTopic, topicPosition, showAssistantIcon } = useSettings() 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( (assistant: Assistant): ItemType[] => [ { @@ -116,7 +128,13 @@ const AssistantItem: FC = ({ assistant, isActive, onSwitch, - {showAssistantIcon && } + {showAssistantIcon && ( + + )} {showAssistantIcon ? assistantName : fullAssistantName} {isActive && ( diff --git a/src/renderer/src/pages/home/Tabs/TopicsTab.tsx b/src/renderer/src/pages/home/Tabs/TopicsTab.tsx index 7d6f4629..aa97cbe2 100644 --- a/src/renderer/src/pages/home/Tabs/TopicsTab.tsx +++ b/src/renderer/src/pages/home/Tabs/TopicsTab.tsx @@ -32,10 +32,11 @@ import { exportTopicToNotion, topicToMarkdown } from '@renderer/utils/export' +import { hasTopicPendingRequests } from '@renderer/utils/queue' import { Dropdown, MenuProps, Tooltip } from 'antd' import dayjs from 'dayjs' 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 styled from 'styled-components' @@ -56,6 +57,28 @@ const Topics: FC = ({ assistant: _assistant, activeTopic, setActiveTopic const [deletingTopicId, setDeletingTopicId] = useState(null) const deleteTimerRef = useRef() + const pendingTopics = useMemo(() => { + return new Set() + }, []) + 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) => { e.stopPropagation() @@ -322,6 +345,7 @@ const Topics: FC = ({ assistant: _assistant, activeTopic, setActiveTopic className={isActive ? 'active' : ''} onClick={() => onSwitchTopic(topic)} style={{ borderRadius }}> + {isPending(topic.id) && !isActive && } {topicName} @@ -395,6 +419,7 @@ const TopicListItem = styled.div` font-family: Ubuntu; cursor: pointer; border: 0.5px solid transparent; + position: relative; .menu { opacity: 0; color: var(--color-text-3); @@ -427,6 +452,19 @@ const TopicName = styled.div` 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` color: var(--color-text-2); font-size: 12px;