From 235122c84397a4617408a152ee404d414f4e4712 Mon Sep 17 00:00:00 2001 From: MyPrototypeWhat <43230886+MyPrototypeWhat@users.noreply.github.com> Date: Sun, 11 May 2025 16:20:28 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20add=20motion=20library=20for=20animatio?= =?UTF-8?q?ns=20and=20enhance=20Spinner=20and=20Messa=E2=80=A6=20(#5869)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit feat: add motion library for animations and enhance Spinner and MessageBlock components - Added 'motion' library to package.json and yarn.lock for animation support. - Refactored Spinner component to utilize motion for animated effects. - Introduced AnimatedBlockWrapper in MessageBlockRenderer for animated transitions. - Updated ThinkingBlock to include animated lightbulb effect during thinking state. --- package.json | 1 + src/renderer/src/components/Spinner.tsx | 75 ++++++++---- .../home/Messages/Blocks/ThinkingBlock.tsx | 38 +++++- .../src/pages/home/Messages/Blocks/index.tsx | 109 ++++++++++++------ yarn.lock | 64 +++++++++- 5 files changed, 224 insertions(+), 63 deletions(-) diff --git a/package.json b/package.json index d6b70fb827..b3b37a694a 100644 --- a/package.json +++ b/package.json @@ -178,6 +178,7 @@ "lru-cache": "^11.1.0", "lucide-react": "^0.487.0", "mime": "^4.0.4", + "motion": "^12.10.5", "npx-scope-finder": "^1.2.0", "openai": "patch:openai@npm%3A4.96.0#~/.yarn/patches/openai-npm-4.96.0-0665b05cb9.patch", "p-queue": "^8.1.0", diff --git a/src/renderer/src/components/Spinner.tsx b/src/renderer/src/components/Spinner.tsx index fb8e3d35e7..74408fc50d 100644 --- a/src/renderer/src/components/Spinner.tsx +++ b/src/renderer/src/components/Spinner.tsx @@ -1,41 +1,68 @@ import { Search } from 'lucide-react' +import { motion } from 'motion/react' import { useTranslation } from 'react-i18next' -import BarLoader from 'react-spinners/BarLoader' -import styled, { css } from 'styled-components' +import styled from 'styled-components' interface Props { text: string } +// Define variants for the spinner animation +const spinnerVariants = { + defaultColor: { + color: '#2a2a2a' + }, + dimmed: { + color: '#8C9296' + } +} + export default function Spinner({ text }: Props) { const { t } = useTranslation() return ( - - - {t(text)} - - + + + {t(text)} + ) } -const baseContainer = css` +// const baseContainer = css` +// display: flex; +// flex-direction: row; +// align-items: center; +// ` + +// const Container = styled.div` +// ${baseContainer} +// background-color: var(--color-background-mute); +// padding: 10px; +// border-radius: 10px; +// margin-bottom: 10px; +// gap: 10px; +// ` + +// const StatusText = styled.div` +// font-size: 14px; +// line-height: 1.6; +// text-decoration: none; +// color: var(--color-text-1); +// ` +const SearchWrapper = styled.div` display: flex; - flex-direction: row; align-items: center; -` - -const Container = styled.div` - ${baseContainer} - background-color: var(--color-background-mute); - padding: 10px; - border-radius: 10px; - margin-bottom: 10px; - gap: 10px; -` - -const StatusText = styled.div` + gap: 4px; font-size: 14px; - line-height: 1.6; - text-decoration: none; - color: var(--color-text-1); + padding: 10px; + padding-left: 0; ` +const Searching = motion.create(SearchWrapper) diff --git a/src/renderer/src/pages/home/Messages/Blocks/ThinkingBlock.tsx b/src/renderer/src/pages/home/Messages/Blocks/ThinkingBlock.tsx index 28702b739a..e95f47a493 100644 --- a/src/renderer/src/pages/home/Messages/Blocks/ThinkingBlock.tsx +++ b/src/renderer/src/pages/home/Messages/Blocks/ThinkingBlock.tsx @@ -2,12 +2,35 @@ import { CheckOutlined } from '@ant-design/icons' import { useSettings } from '@renderer/hooks/useSettings' import { MessageBlockStatus, type ThinkingMessageBlock } from '@renderer/types/newMessage' import { Collapse, message as antdMessage, Tooltip } from 'antd' +import { Lightbulb } from 'lucide-react' +import { motion } from 'motion/react' import { memo, useCallback, useEffect, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' -import BarLoader from 'react-spinners/BarLoader' import styled from 'styled-components' import Markdown from '../../Markdown/Markdown' + +// Define variants outside the component if they don't depend on component's props/state directly +// or inside if they do (though for this case, outside is fine). +const lightbulbVariants = { + thinking: { + opacity: [1, 0.2, 1], + transition: { + duration: 1.2, + ease: 'easeInOut', + times: [0, 0.5, 1], + repeat: Infinity + } + }, + idle: { + opacity: 1, + transition: { + duration: 0.3, // Smooth transition to idle state + ease: 'easeInOut' + } + } +} + interface Props { block: ThinkingMessageBlock } @@ -63,17 +86,25 @@ const ThinkingBlock: React.FC = ({ block }) => { size="small" onChange={() => setActiveKey((key) => (key ? '' : 'thought'))} className="message-thought-container" + expandIconPosition="end" items={[ { key: 'thought', label: ( + + + {t(isThinking ? 'chat.thinking' : 'chat.deeply_thought', { seconds: thinkingTimeSeconds })} - {isThinking && } + {/* {isThinking && } */} {!isThinking && ( = ({ block }) => { const CollapseContainer = styled(Collapse)` margin-bottom: 15px; + max-width: 960px; ` const MessageTitleLabel = styled.div` @@ -111,7 +143,7 @@ const MessageTitleLabel = styled.div` flex-direction: row; align-items: center; height: 22px; - gap: 15px; + gap: 4px; ` const ThinkingText = styled.span` diff --git a/src/renderer/src/pages/home/Messages/Blocks/index.tsx b/src/renderer/src/pages/home/Messages/Blocks/index.tsx index 12e0966c9e..c7233089bc 100644 --- a/src/renderer/src/pages/home/Messages/Blocks/index.tsx +++ b/src/renderer/src/pages/home/Messages/Blocks/index.tsx @@ -1,17 +1,8 @@ import type { RootState } from '@renderer/store' import { messageBlocksSelectors } from '@renderer/store/messageBlock' -import type { - ErrorMessageBlock, - FileMessageBlock, - ImageMessageBlock, - MainTextMessageBlock, - Message, - MessageBlock, - PlaceholderMessageBlock, - ThinkingMessageBlock, - TranslationMessageBlock -} from '@renderer/types/newMessage' +import type { ImageMessageBlock, MainTextMessageBlock, Message, MessageBlock } from '@renderer/types/newMessage' import { MessageBlockStatus, MessageBlockType } from '@renderer/types/newMessage' +import { AnimatePresence, motion } from 'motion/react' import React, { useMemo } from 'react' import { useSelector } from 'react-redux' import styled from 'styled-components' @@ -26,8 +17,41 @@ import ThinkingBlock from './ThinkingBlock' import ToolBlock from './ToolBlock' import TranslationBlock from './TranslationBlock' +interface AnimatedBlockWrapperProps { + children: React.ReactNode + enableAnimation: boolean +} + +const blockWrapperVariants = { + visible: { + opacity: 1, + x: 0, + transition: { duration: 0.3, type: 'spring', bounce: 0 } + }, + hidden: { + opacity: 0, + x: 10 + }, + static: { + opacity: 1, + x: 0, + transition: { duration: 0 } + } +} + +const AnimatedBlockWrapper: React.FC = ({ children, enableAnimation }) => { + return ( + + {children} + + ) +} + interface Props { - blocks: MessageBlock[] | string[] // 可以接收块ID数组或MessageBlock数组 + blocks: string[] // 可以接收块ID数组或MessageBlock数组 messageStatus?: Message['status'] message: Message } @@ -54,26 +78,30 @@ const MessageBlockRenderer: React.FC = ({ blocks, message }) => { // 根据blocks类型处理渲染数据 const renderedBlocks = blocks.map((blockId) => blockEntities[blockId]).filter(Boolean) const groupedBlocks = useMemo(() => filterImageBlockGroups(renderedBlocks), [renderedBlocks]) - return ( - <> + {groupedBlocks.map((block) => { if (Array.isArray(block)) { + const groupKey = block.map((imageBlock) => imageBlock.id).join('-') return ( - imageBlock.id).join('-')}> - {block.map((imageBlock) => ( - - ))} - + + + {block.map((imageBlock) => ( + + ))} + + ) } + let blockComponent: React.ReactNode = null + switch (block.type) { case MessageBlockType.UNKNOWN: if (block.status === MessageBlockStatus.PROCESSING) { - return + blockComponent = } - return null + break case MessageBlockType.MAIN_TEXT: case MessageBlockType.CODE: { const mainTextBlock = block as MainTextMessageBlock @@ -82,7 +110,7 @@ const MessageBlockRenderer: React.FC = ({ blocks, message }) => { // No longer need to retrieve the full citation block here // const citationBlock = citationBlockId ? (blockEntities[citationBlockId] as CitationMessageBlock) : undefined - return ( + blockComponent = ( = ({ blocks, message }) => { role={message.role} /> ) + break } case MessageBlockType.IMAGE: - return + blockComponent = + break case MessageBlockType.FILE: - return + blockComponent = + break case MessageBlockType.TOOL: - return + blockComponent = + break case MessageBlockType.CITATION: - return + blockComponent = + break case MessageBlockType.ERROR: - return + blockComponent = + break case MessageBlockType.THINKING: - return - // case MessageBlockType.CODE: - // return + blockComponent = + break case MessageBlockType.TRANSLATION: - return + blockComponent = + break default: - // Cast block to any for console.warn to fix linter error console.warn('Unsupported block type in MessageBlockRenderer:', (block as any).type, block) - return null + break } + + return ( + + {blockComponent} + + ) })} - + ) } diff --git a/yarn.lock b/yarn.lock index 0ecf3b0fe4..3aceff4aa9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4488,6 +4488,7 @@ __metadata: lucide-react: "npm:^0.487.0" markdown-it: "npm:^14.1.0" mime: "npm:^4.0.4" + motion: "npm:^12.10.5" node-stream-zip: "npm:^1.15.0" npx-scope-finder: "npm:^1.2.0" officeparser: "npm:^4.1.1" @@ -8559,6 +8560,28 @@ __metadata: languageName: node linkType: hard +"framer-motion@npm:^12.10.5": + version: 12.10.5 + resolution: "framer-motion@npm:12.10.5" + dependencies: + motion-dom: "npm:^12.10.5" + motion-utils: "npm:^12.9.4" + tslib: "npm:^2.4.0" + peerDependencies: + "@emotion/is-prop-valid": "*" + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + "@emotion/is-prop-valid": + optional: true + react: + optional: true + react-dom: + optional: true + checksum: 10c0/a24a44b7a1b21e347f93f9ec3c1218b9ebf2b2bc2883c26ab9951e19a62fdc2e03f80a57d0c78eaf408d098ed6f0fbcae48207313921c1f5462eb04296adf55b + languageName: node + linkType: hard + "fresh@npm:^2.0.0": version: 2.0.0 resolution: "fresh@npm:2.0.0" @@ -12414,6 +12437,43 @@ __metadata: languageName: node linkType: hard +"motion-dom@npm:^12.10.5": + version: 12.10.5 + resolution: "motion-dom@npm:12.10.5" + dependencies: + motion-utils: "npm:^12.9.4" + checksum: 10c0/2c362eb94c941bbbc42288a6738b8c7a11933687b3b20aa6c9f2c3dedc69e5c7995c7348499b535f8abe5ed9ea81d88f9eb2f98b69f5012bcd80b8f7a64a1c2c + languageName: node + linkType: hard + +"motion-utils@npm:^12.9.4": + version: 12.9.4 + resolution: "motion-utils@npm:12.9.4" + checksum: 10c0/b6783babfd1282ad320585f7cdac9fe7a1f97b39e07d12a500d3709534441bd9d49b556fa1cd838d1bde188570d4ab6b4c5aa9d297f7f5aa9dc16d600c17afdc + languageName: node + linkType: hard + +"motion@npm:^12.10.5": + version: 12.10.5 + resolution: "motion@npm:12.10.5" + dependencies: + framer-motion: "npm:^12.10.5" + tslib: "npm:^2.4.0" + peerDependencies: + "@emotion/is-prop-valid": "*" + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + "@emotion/is-prop-valid": + optional: true + react: + optional: true + react-dom: + optional: true + checksum: 10c0/d8f1755a565332e6122e2079e164026b945eda34827170f2615999d74d3df2ad77984ca55304d7682b97a2ccf83c33508d234af619b043cd18056047884396d1 + languageName: node + linkType: hard + "mri@npm:1.1.4": version: 1.1.4 resolution: "mri@npm:1.1.4" @@ -12940,7 +13000,7 @@ __metadata: "openai@patch:openai@npm%3A4.96.0#~/.yarn/patches/openai-npm-4.96.0-0665b05cb9.patch": version: 4.96.0 - resolution: "openai@patch:openai@npm%3A4.96.0#~/.yarn/patches/openai-npm-4.96.0-0665b05cb9.patch::version=4.96.0&hash=645779" + resolution: "openai@patch:openai@npm%3A4.96.0#~/.yarn/patches/openai-npm-4.96.0-0665b05cb9.patch::version=4.96.0&hash=6bc976" dependencies: "@types/node": "npm:^18.11.18" "@types/node-fetch": "npm:^2.6.4" @@ -12959,7 +13019,7 @@ __metadata: optional: true bin: openai: bin/cli - checksum: 10c0/8c16fcf1812294220eddd4616e298c2af87398acb479287b7565548c8c1979c6d5c487fb7a9c25b0ac59f778de74c23d94ce1a34362c49260ae7a14acf22abc2 + checksum: 10c0/e50e4b9b60e94fadaca541cf2c36a12c55221555dd2ce977738e13978b7187504263f2e31b4641f2b6e70fce562b4e1fa2affd68caeca21248ddfa8847eeb003 languageName: node linkType: hard