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