mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2026-01-01 09:49:03 +08:00
feat: thinking effect (#8081)
* feat(i18n): add smooth stream output translations for multiple languages * feat(ThinkingBlock): integrate MarqueeComponent for enhanced message display * refactor(i18n): remove smooth stream output references from translations and components * refactor(typingOutput): enhance typing output logic and add debugging information * refactor(Markdown): consolidate markdown utility imports for cleaner code * feat(styles): add new styles for dropdown menus, popovers, and modals * test(ThinkingBlock): enhance tests for streaming status and content collapse behavior * refactor(typingOutput): remove debugging console log from outputNextChar function * refactor(MarqueeComponent): comment out blur effect for last marquee item and adjust ThinkingBlock margin * style(ThinkingBlock): update snapshot to include margin-top for improved layout * refactor(typingOutput): 修改流式输出逻辑以支持队列长度检查 * refactor(Markdown): simplify useTypingOutput by removing isStreaming parameter * test(Markdown): comment out re-render tests for content changes * test(Markdown): remove commented-out re-render tests for content changes * feat(ThinkingEffect): implement ThinkingEffect component for dynamic message display - Introduced ThinkingEffect component to enhance the visual representation of thinking states. - Integrated the new component into ThinkingBlock, replacing MarqueeComponent for improved functionality. - Added animations and dynamic height adjustments based on message content and expansion state. * test(ThinkingBlock): update mocks for ThinkingEffect and motion components in tests * fix: Delete unnecessary comments
This commit is contained in:
parent
6952bea6e1
commit
7961ba87ed
@ -136,17 +136,16 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.ant-collapse {
|
.ant-collapse:not(.ant-collapse-ghost) {
|
||||||
border: 1px solid var(--color-border);
|
border: 1px solid var(--color-border);
|
||||||
.ant-color-picker & {
|
.ant-color-picker & {
|
||||||
border: none;
|
border: none;
|
||||||
}
|
}
|
||||||
}
|
.ant-collapse-content {
|
||||||
|
border-top: 0.5px solid var(--color-border) !important;
|
||||||
.ant-collapse-content {
|
.ant-color-picker & {
|
||||||
border-top: 0.5px solid var(--color-border) !important;
|
border-top: none !important;
|
||||||
.ant-color-picker & {
|
}
|
||||||
border-top: none !important;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
179
src/renderer/src/components/ThinkingEffect.tsx
Normal file
179
src/renderer/src/components/ThinkingEffect.tsx
Normal file
@ -0,0 +1,179 @@
|
|||||||
|
import { lightbulbVariants } from '@renderer/utils/motionVariants'
|
||||||
|
import { isEqual } from 'lodash'
|
||||||
|
import { ChevronRight, Lightbulb } from 'lucide-react'
|
||||||
|
import { AnimatePresence, motion } from 'motion/react'
|
||||||
|
import React, { useEffect, useMemo, useState } from 'react'
|
||||||
|
import styled from 'styled-components'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
isThinking: boolean
|
||||||
|
thinkingTimeText: React.ReactNode
|
||||||
|
content: string
|
||||||
|
expanded: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const ThinkingEffect: React.FC<Props> = ({ isThinking, thinkingTimeText, content, expanded }) => {
|
||||||
|
const [messages, setMessages] = useState<string[]>([])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const allLines = (content || '').split('\n')
|
||||||
|
const newMessages = isThinking ? allLines.slice(0, -1) : allLines
|
||||||
|
const validMessages = newMessages.filter((line) => line.trim() !== '')
|
||||||
|
|
||||||
|
if (!isEqual(messages, validMessages)) {
|
||||||
|
setMessages(validMessages)
|
||||||
|
}
|
||||||
|
}, [content, isThinking, messages])
|
||||||
|
|
||||||
|
const lineHeight = 16
|
||||||
|
const containerHeight = useMemo(() => {
|
||||||
|
if (expanded) return lineHeight * 3
|
||||||
|
return Math.min(80, Math.max(messages.length + 2, 3) * lineHeight)
|
||||||
|
}, [expanded, messages.length])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ThinkingContainer style={{ height: containerHeight }} className={expanded ? 'expanded' : ''}>
|
||||||
|
<LoadingContainer className={expanded || !messages.length ? 'expanded' : ''}>
|
||||||
|
<motion.div variants={lightbulbVariants} animate={isThinking ? 'active' : 'idle'} initial="idle">
|
||||||
|
<Lightbulb size={expanded || !messages.length ? 20 : 30} style={{ transition: 'width,height, 150ms' }} />
|
||||||
|
</motion.div>
|
||||||
|
</LoadingContainer>
|
||||||
|
|
||||||
|
<TextContainer>
|
||||||
|
<Title className={expanded || !messages.length ? 'expanded' : ''}>{thinkingTimeText}</Title>
|
||||||
|
|
||||||
|
{!expanded && (
|
||||||
|
<Content>
|
||||||
|
<AnimatePresence>
|
||||||
|
{messages.map((message, index) => {
|
||||||
|
const finalY = containerHeight - (messages.length - index) * lineHeight - 4
|
||||||
|
|
||||||
|
if (index < messages.length - 5) return null
|
||||||
|
|
||||||
|
const opacity = (() => {
|
||||||
|
const distanceFromLast = messages.length - 1 - index
|
||||||
|
if (distanceFromLast === 0) return 1
|
||||||
|
if (distanceFromLast === 1) return 0.6
|
||||||
|
if (distanceFromLast === 2) return 0.4
|
||||||
|
return 0
|
||||||
|
})()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ContentLineMotion
|
||||||
|
key={`${index}-${message}`}
|
||||||
|
initial={{
|
||||||
|
opacity: 1,
|
||||||
|
y: index === messages.length - 1 ? containerHeight : finalY + lineHeight,
|
||||||
|
height: lineHeight
|
||||||
|
}}
|
||||||
|
animate={{
|
||||||
|
opacity,
|
||||||
|
y: finalY
|
||||||
|
}}
|
||||||
|
transition={{
|
||||||
|
duration: 0.15,
|
||||||
|
ease: 'linear'
|
||||||
|
}}>
|
||||||
|
{message}
|
||||||
|
</ContentLineMotion>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</AnimatePresence>
|
||||||
|
</Content>
|
||||||
|
)}
|
||||||
|
</TextContainer>
|
||||||
|
<ArrowContainer className={expanded ? 'expanded' : ''}>
|
||||||
|
<ChevronRight size={20} color="var(--color-text-3)" strokeWidth={1.2} />
|
||||||
|
</ArrowContainer>
|
||||||
|
</ThinkingContainer>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const ThinkingContainer = styled(motion.div)`
|
||||||
|
width: 100%;
|
||||||
|
border-radius: 12px;
|
||||||
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
border: 0.5px solid var(--color-border);
|
||||||
|
transition: height, border-radius, 150ms;
|
||||||
|
pointer-events: none;
|
||||||
|
user-select: none;
|
||||||
|
&.expanded {
|
||||||
|
border-radius: 12px 12px 0 0;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const Title = styled.div`
|
||||||
|
position: absolute;
|
||||||
|
inset: 0 0 auto 0;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
padding: 4px 0 30px;
|
||||||
|
z-index: 99;
|
||||||
|
transition: padding-top 150ms;
|
||||||
|
&.expanded {
|
||||||
|
padding-top: 14px;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const LoadingContainer = styled.div`
|
||||||
|
width: 60px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
height: 100%;
|
||||||
|
flex-shrink: 0;
|
||||||
|
position: relative;
|
||||||
|
padding-left: 5px;
|
||||||
|
transition: width 150ms;
|
||||||
|
> div {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
&.expanded {
|
||||||
|
width: 40px;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const TextContainer = styled.div`
|
||||||
|
flex: 1;
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
`
|
||||||
|
|
||||||
|
const Content = styled(motion.div)`
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
`
|
||||||
|
|
||||||
|
const ContentLineMotion = styled(motion.div)`
|
||||||
|
width: 100%;
|
||||||
|
line-height: 16px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--color-text-2);
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
position: absolute;
|
||||||
|
`
|
||||||
|
|
||||||
|
const ArrowContainer = styled.div`
|
||||||
|
width: 40px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
height: 100%;
|
||||||
|
flex-shrink: 0;
|
||||||
|
position: relative;
|
||||||
|
color: var(--color-border);
|
||||||
|
transition: transform 150ms;
|
||||||
|
&.expanded {
|
||||||
|
transform: rotate(90deg);
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
export default ThinkingEffect
|
||||||
@ -1,10 +1,8 @@
|
|||||||
import { CheckOutlined } from '@ant-design/icons'
|
import { CheckOutlined } from '@ant-design/icons'
|
||||||
|
import ThinkingEffect from '@renderer/components/ThinkingEffect'
|
||||||
import { useSettings } from '@renderer/hooks/useSettings'
|
import { useSettings } from '@renderer/hooks/useSettings'
|
||||||
import { MessageBlockStatus, type ThinkingMessageBlock } from '@renderer/types/newMessage'
|
import { MessageBlockStatus, type ThinkingMessageBlock } from '@renderer/types/newMessage'
|
||||||
import { lightbulbVariants } from '@renderer/utils/motionVariants'
|
|
||||||
import { Collapse, message as antdMessage, Tooltip } from 'antd'
|
import { Collapse, message as antdMessage, Tooltip } from 'antd'
|
||||||
import { ChevronRight, Lightbulb } from 'lucide-react'
|
|
||||||
import { motion } from 'motion/react'
|
|
||||||
import { memo, useCallback, useEffect, useMemo, useState } from 'react'
|
import { memo, useCallback, useEffect, useMemo, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
@ -24,7 +22,7 @@ const ThinkingBlock: React.FC<Props> = ({ block }) => {
|
|||||||
const isThinking = useMemo(() => block.status === MessageBlockStatus.STREAMING, [block.status])
|
const isThinking = useMemo(() => block.status === MessageBlockStatus.STREAMING, [block.status])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isThinking && thoughtAutoCollapse) {
|
if (thoughtAutoCollapse) {
|
||||||
setActiveKey('')
|
setActiveKey('')
|
||||||
} else {
|
} else {
|
||||||
setActiveKey('thought')
|
setActiveKey('thought')
|
||||||
@ -57,31 +55,27 @@ const ThinkingBlock: React.FC<Props> = ({ block }) => {
|
|||||||
size="small"
|
size="small"
|
||||||
onChange={() => setActiveKey((key) => (key ? '' : 'thought'))}
|
onChange={() => setActiveKey((key) => (key ? '' : 'thought'))}
|
||||||
className="message-thought-container"
|
className="message-thought-container"
|
||||||
expandIcon={({ isActive }) => (
|
ghost
|
||||||
<ChevronRight
|
|
||||||
color="var(--color-text-3)"
|
|
||||||
size={16}
|
|
||||||
strokeWidth={1.5}
|
|
||||||
style={{ transform: isActive ? 'rotate(90deg)' : 'rotate(0deg)' }}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
expandIconPosition="end"
|
|
||||||
items={[
|
items={[
|
||||||
{
|
{
|
||||||
key: 'thought',
|
key: 'thought',
|
||||||
label: (
|
label: (
|
||||||
<MessageTitleLabel>
|
<ThinkingEffect
|
||||||
<motion.span
|
expanded={activeKey === 'thought'}
|
||||||
style={{ height: '18px' }}
|
isThinking={isThinking}
|
||||||
variants={lightbulbVariants}
|
thinkingTimeText={
|
||||||
animate={isThinking ? 'active' : 'idle'}
|
|
||||||
initial="idle">
|
|
||||||
<Lightbulb size={18} />
|
|
||||||
</motion.span>
|
|
||||||
<ThinkingText>
|
|
||||||
<ThinkingTimeSeconds blockThinkingTime={block.thinking_millsec} isThinking={isThinking} />
|
<ThinkingTimeSeconds blockThinkingTime={block.thinking_millsec} isThinking={isThinking} />
|
||||||
</ThinkingText>
|
}
|
||||||
{/* {isThinking && <BarLoader color="#9254de" />} */}
|
content={block.content}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
children: (
|
||||||
|
// FIXME: 临时兼容
|
||||||
|
<ThinkingContent
|
||||||
|
style={{
|
||||||
|
fontFamily: messageFont === 'serif' ? 'var(--font-family-serif)' : 'var(--font-family)',
|
||||||
|
fontSize
|
||||||
|
}}>
|
||||||
{!isThinking && (
|
{!isThinking && (
|
||||||
<Tooltip title={t('common.copy')} mouseEnterDelay={0.8}>
|
<Tooltip title={t('common.copy')} mouseEnterDelay={0.8}>
|
||||||
<ActionButton
|
<ActionButton
|
||||||
@ -96,18 +90,10 @@ const ThinkingBlock: React.FC<Props> = ({ block }) => {
|
|||||||
</ActionButton>
|
</ActionButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
</MessageTitleLabel>
|
|
||||||
),
|
|
||||||
children: (
|
|
||||||
// FIXME: 临时兼容
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
fontFamily: messageFont === 'serif' ? 'var(--font-family-serif)' : 'var(--font-family)',
|
|
||||||
fontSize
|
|
||||||
}}>
|
|
||||||
<Markdown block={block} />
|
<Markdown block={block} />
|
||||||
</div>
|
</ThinkingContent>
|
||||||
)
|
),
|
||||||
|
showArrow: false
|
||||||
}
|
}
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
@ -150,20 +136,22 @@ const ThinkingTimeSeconds = memo(
|
|||||||
)
|
)
|
||||||
|
|
||||||
const CollapseContainer = styled(Collapse)`
|
const CollapseContainer = styled(Collapse)`
|
||||||
margin: 15px 0;
|
margin-top: 15px;
|
||||||
margin-top: 5px;
|
margin-bottom: 15px;
|
||||||
|
.ant-collapse-header {
|
||||||
|
padding: 0 !important;
|
||||||
|
}
|
||||||
|
.ant-collapse-content-box {
|
||||||
|
padding: 16px !important;
|
||||||
|
border-width: 0 0.5px 0.5px 0.5px;
|
||||||
|
border-style: solid;
|
||||||
|
border-color: var(--color-border);
|
||||||
|
border-radius: 0 0 12px 12px;
|
||||||
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
const MessageTitleLabel = styled.div`
|
const ThinkingContent = styled.div`
|
||||||
display: flex;
|
position: relative;
|
||||||
flex-direction: row;
|
|
||||||
align-items: center;
|
|
||||||
height: 22px;
|
|
||||||
gap: 4px;
|
|
||||||
`
|
|
||||||
|
|
||||||
const ThinkingText = styled.span`
|
|
||||||
color: var(--color-text-2);
|
|
||||||
`
|
`
|
||||||
|
|
||||||
const ActionButton = styled.button`
|
const ActionButton = styled.button`
|
||||||
@ -178,6 +166,9 @@ const ActionButton = styled.button`
|
|||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
opacity: 0.6;
|
opacity: 0.6;
|
||||||
transition: all 0.3s;
|
transition: all 0.3s;
|
||||||
|
position: absolute;
|
||||||
|
right: -12px;
|
||||||
|
top: -12px;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
|
|||||||
@ -63,12 +63,15 @@ vi.mock('lucide-react', () => ({
|
|||||||
<span data-testid="lightbulb-icon" data-size={size}>
|
<span data-testid="lightbulb-icon" data-size={size}>
|
||||||
💡
|
💡
|
||||||
</span>
|
</span>
|
||||||
)
|
),
|
||||||
|
ChevronRight: (props: any) => <svg data-testid="chevron-right-icon" {...props} />
|
||||||
}))
|
}))
|
||||||
|
|
||||||
// Mock motion
|
// Mock motion
|
||||||
vi.mock('motion/react', () => ({
|
vi.mock('motion/react', () => ({
|
||||||
|
AnimatePresence: ({ children }: any) => <div data-testid="animate-presence">{children}</div>,
|
||||||
motion: {
|
motion: {
|
||||||
|
div: (props: any) => <div {...props} />,
|
||||||
span: ({ children, variants, animate, initial, style }: any) => (
|
span: ({ children, variants, animate, initial, style }: any) => (
|
||||||
<span
|
<span
|
||||||
data-testid="motion-span"
|
data-testid="motion-span"
|
||||||
@ -100,6 +103,20 @@ vi.mock('@renderer/pages/home/Markdown/Markdown', () => ({
|
|||||||
)
|
)
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
// Mock ThinkingEffect component
|
||||||
|
vi.mock('@renderer/components/ThinkingEffect', () => ({
|
||||||
|
__esModule: true,
|
||||||
|
default: ({ isThinking, thinkingTimeText, content, expanded }: any) => (
|
||||||
|
<div
|
||||||
|
data-testid="mock-marquee-component"
|
||||||
|
data-is-thinking={isThinking}
|
||||||
|
data-expanded={expanded}
|
||||||
|
data-content={content}>
|
||||||
|
<div data-testid="thinking-time-text">{thinkingTimeText}</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}))
|
||||||
|
|
||||||
describe('ThinkingBlock', () => {
|
describe('ThinkingBlock', () => {
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
vi.useFakeTimers()
|
vi.useFakeTimers()
|
||||||
@ -153,7 +170,7 @@ describe('ThinkingBlock', () => {
|
|||||||
|
|
||||||
const getThinkingContent = () => screen.queryByText(/markdown:/i)
|
const getThinkingContent = () => screen.queryByText(/markdown:/i)
|
||||||
const getCopyButton = () => screen.queryByRole('button', { name: /copy/i })
|
const getCopyButton = () => screen.queryByRole('button', { name: /copy/i })
|
||||||
const getThinkingTimeText = () => screen.getByText(/thinking|thought/i)
|
const getThinkingTimeText = () => screen.getByTestId('thinking-time-text')
|
||||||
|
|
||||||
describe('basic rendering', () => {
|
describe('basic rendering', () => {
|
||||||
it('should render thinking content when provided', () => {
|
it('should render thinking content when provided', () => {
|
||||||
@ -162,7 +179,7 @@ describe('ThinkingBlock', () => {
|
|||||||
|
|
||||||
// User should see the thinking content
|
// User should see the thinking content
|
||||||
expect(screen.getByText('Markdown: Deep thoughts about AI')).toBeInTheDocument()
|
expect(screen.getByText('Markdown: Deep thoughts about AI')).toBeInTheDocument()
|
||||||
expect(screen.getByTestId('lightbulb-icon')).toBeInTheDocument()
|
expect(screen.getByTestId('mock-marquee-component')).toBeInTheDocument()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should not render when content is empty', () => {
|
it('should not render when content is empty', () => {
|
||||||
@ -332,14 +349,14 @@ describe('ThinkingBlock', () => {
|
|||||||
const streamingBlock = createThinkingBlock({ status: MessageBlockStatus.STREAMING })
|
const streamingBlock = createThinkingBlock({ status: MessageBlockStatus.STREAMING })
|
||||||
const { rerender } = renderThinkingBlock(streamingBlock)
|
const { rerender } = renderThinkingBlock(streamingBlock)
|
||||||
|
|
||||||
// Should be expanded while thinking
|
// With thoughtAutoCollapse enabled, it should be collapsed even while thinking
|
||||||
expect(getThinkingContent()).toBeInTheDocument()
|
expect(getThinkingContent()).not.toBeInTheDocument()
|
||||||
|
|
||||||
// Stop thinking
|
// Stop thinking
|
||||||
const completedBlock = createThinkingBlock({ status: MessageBlockStatus.SUCCESS })
|
const completedBlock = createThinkingBlock({ status: MessageBlockStatus.SUCCESS })
|
||||||
rerender(<ThinkingBlock block={completedBlock} />)
|
rerender(<ThinkingBlock block={completedBlock} />)
|
||||||
|
|
||||||
// Should be collapsed after thinking completes
|
// Should remain collapsed after thinking completes
|
||||||
expect(getThinkingContent()).not.toBeInTheDocument()
|
expect(getThinkingContent()).not.toBeInTheDocument()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@ -2,23 +2,27 @@
|
|||||||
|
|
||||||
exports[`ThinkingBlock > basic rendering > should match snapshot 1`] = `
|
exports[`ThinkingBlock > basic rendering > should match snapshot 1`] = `
|
||||||
.c0 {
|
.c0 {
|
||||||
margin: 15px 0;
|
margin-top: 15px;
|
||||||
margin-top: 5px;
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.c0 .ant-collapse-header {
|
||||||
|
padding: 0!important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.c0 .ant-collapse-content-box {
|
||||||
|
padding: 16px!important;
|
||||||
|
border-width: 0 0.5px 0.5px 0.5px;
|
||||||
|
border-style: solid;
|
||||||
|
border-color: var(--color-border);
|
||||||
|
border-radius: 0 0 12px 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.c1 {
|
.c1 {
|
||||||
display: flex;
|
position: relative;
|
||||||
flex-direction: row;
|
|
||||||
align-items: center;
|
|
||||||
height: 22px;
|
|
||||||
gap: 4px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.c2 {
|
.c2 {
|
||||||
color: var(--color-text-2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.c3 {
|
|
||||||
background: none;
|
background: none;
|
||||||
border: none;
|
border: none;
|
||||||
color: var(--color-text-2);
|
color: var(--color-text-2);
|
||||||
@ -30,26 +34,28 @@ exports[`ThinkingBlock > basic rendering > should match snapshot 1`] = `
|
|||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
opacity: 0.6;
|
opacity: 0.6;
|
||||||
transition: all 0.3s;
|
transition: all 0.3s;
|
||||||
|
position: absolute;
|
||||||
|
right: -12px;
|
||||||
|
top: -12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.c3:hover {
|
.c2:hover {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
color: var(--color-text);
|
color: var(--color-text);
|
||||||
}
|
}
|
||||||
|
|
||||||
.c3:focus-visible {
|
.c2:focus-visible {
|
||||||
outline: 2px solid var(--color-primary);
|
outline: 2px solid var(--color-primary);
|
||||||
outline-offset: 2px;
|
outline-offset: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.c3 .iconfont {
|
.c2 .iconfont {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="c0 message-thought-container"
|
class="c0 message-thought-container"
|
||||||
data-active-key="thought"
|
data-active-key="thought"
|
||||||
data-expand-icon-position="end"
|
|
||||||
data-size="small"
|
data-size="small"
|
||||||
data-testid="collapse-container"
|
data-testid="collapse-container"
|
||||||
>
|
>
|
||||||
@ -60,40 +66,15 @@ exports[`ThinkingBlock > basic rendering > should match snapshot 1`] = `
|
|||||||
data-testid="collapse-header-thought"
|
data-testid="collapse-header-thought"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="c1"
|
data-content="I need to think about this carefully..."
|
||||||
|
data-expanded="true"
|
||||||
|
data-is-thinking="false"
|
||||||
|
data-testid="mock-marquee-component"
|
||||||
>
|
>
|
||||||
<span
|
<div
|
||||||
data-animate="idle"
|
data-testid="thinking-time-text"
|
||||||
data-initial="idle"
|
|
||||||
data-testid="motion-span"
|
|
||||||
data-variants="{"active":{"rotate":10,"scale":1.1},"idle":{"rotate":0,"scale":1}}"
|
|
||||||
style="height: 18px;"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
data-size="18"
|
|
||||||
data-testid="lightbulb-icon"
|
|
||||||
>
|
|
||||||
💡
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
<span
|
|
||||||
class="c2"
|
|
||||||
>
|
>
|
||||||
Thought for 5.0s
|
Thought for 5.0s
|
||||||
</span>
|
|
||||||
<div
|
|
||||||
data-mouse-enter-delay="0.8"
|
|
||||||
data-testid="tooltip"
|
|
||||||
title="Copy"
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
aria-label="Copy"
|
|
||||||
class="c3 message-action-button"
|
|
||||||
>
|
|
||||||
<i
|
|
||||||
class="iconfont icon-copy"
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -101,8 +82,23 @@ exports[`ThinkingBlock > basic rendering > should match snapshot 1`] = `
|
|||||||
data-testid="collapse-content-thought"
|
data-testid="collapse-content-thought"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
|
class="c1"
|
||||||
style="font-family: var(--font-family); font-size: 14px;"
|
style="font-family: var(--font-family); font-size: 14px;"
|
||||||
>
|
>
|
||||||
|
<div
|
||||||
|
data-mouse-enter-delay="0.8"
|
||||||
|
data-testid="tooltip"
|
||||||
|
title="Copy"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
aria-label="Copy"
|
||||||
|
class="c2 message-action-button"
|
||||||
|
>
|
||||||
|
<i
|
||||||
|
class="iconfont icon-copy"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
<div
|
<div
|
||||||
data-block-id="test-thinking-block-1"
|
data-block-id="test-thinking-block-1"
|
||||||
data-testid="mock-markdown"
|
data-testid="mock-markdown"
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user