refactor(ui): update migration status and enhance component implementations

- Increased the refactored component count to 18 and reduced pending migrations to 184 in the migration status files.
- Improved the Ellipsis, ListItem, MaxContextCount, and ThinkingEffect components by simplifying their structure and enhancing styling.
- Updated the tsconfig.json to adjust the root directory for better project organization.
- Removed unnecessary alias configurations in tsdown.config.ts for cleaner setup.
- Added new stories for Ellipsis and ListItem components to improve documentation and showcase their usage.
This commit is contained in:
MyPrototypeWhat 2025-09-16 19:19:58 +08:00
parent bd7cd22220
commit 59bf94b118
12 changed files with 1328 additions and 236 deletions

View File

@ -50,8 +50,8 @@ function MyComponent() {
- **总组件数**: 236
- **已迁移**: 34
- **已重构**: 14
- **待迁移**: 188
- **已重构**: 18
- **待迁移**: 184
## 组件状态表
@ -69,12 +69,12 @@ function MyComponent() {
| | TextBadge | ✅ | ✅ | 文本徽标 |
| | CustomCollapse | ✅ | ✅ | 自定义折叠面板 |
| **display** | | | | 显示组件 |
| | Ellipsis | ✅ | | 文本省略 |
| | Ellipsis | ✅ | | 文本省略 |
| | ExpandableText | ✅ | ✅ | 可展开文本 |
| | ThinkingEffect | ✅ | | 思考效果动画 |
| | ThinkingEffect | ✅ | | 思考效果动画 |
| | EmojiAvatar | ✅ | ✅ | 表情头像 |
| | ListItem | ✅ | | 列表项 |
| | MaxContextCount | ✅ | | 最大上下文数显示 |
| | ListItem | ✅ | | 列表项 |
| | MaxContextCount | ✅ | | 最大上下文数显示 |
| | ProviderAvatar | ✅ | ✅ | 提供者头像 |
| | CodeViewer | ❌ | ❌ | 代码查看器 (外部依赖) |
| | OGCard | ❌ | ❌ | OG 卡片 |

View File

@ -49,8 +49,8 @@ When submitting PRs, please place components in the correct directory based on t
- **Total Components**: 236
- **Migrated**: 34
- **Refactored**: 14
- **Pending Migration**: 188
- **Refactored**: 18
- **Pending Migration**: 184
## Component Status Table
@ -68,12 +68,12 @@ When submitting PRs, please place components in the correct directory based on t
| | TextBadge | ✅ | ✅ | Text badge |
| | CustomCollapse | ✅ | ✅ | Custom collapse panel |
| **display** | | | | Display components |
| | Ellipsis | ✅ | | Text ellipsis |
| | Ellipsis | ✅ | | Text ellipsis |
| | ExpandableText | ✅ | ✅ | Expandable text |
| | ThinkingEffect | ✅ | | Thinking effect animation |
| | ThinkingEffect | ✅ | | Thinking effect animation |
| | EmojiAvatar | ✅ | ✅ | Emoji avatar |
| | ListItem | ✅ | | List item |
| | MaxContextCount | ✅ | | Max context count display |
| | ListItem | ✅ | | List item |
| | MaxContextCount | ✅ | | Max context count display |
| | ProviderAvatar | ✅ | ✅ | Provider avatar |
| | CodeViewer | ❌ | ❌ | Code viewer (external deps) |
| | OGCard | ❌ | ❌ | OG card |

View File

@ -1,36 +1,28 @@
// Original: src/renderer/src/components/Ellipsis/index.tsx
import type { HTMLAttributes } from 'react'
import styled, { css } from 'styled-components'
import { cn } from '../../../utils'
type Props = {
maxLine?: number
className?: string
ref?: React.Ref<HTMLDivElement>
} & HTMLAttributes<HTMLDivElement>
const Ellipsis = (props: Props) => {
const { maxLine = 1, children, ...rest } = props
const { maxLine = 1, children, className, ref, ...rest } = props
const ellipsisClasses = cn(
'overflow-hidden text-ellipsis',
maxLine > 1 ? `line-clamp-${maxLine} break-words` : 'block whitespace-nowrap',
className
)
return (
<EllipsisContainer $maxLine={maxLine} {...rest}>
<div ref={ref} className={ellipsisClasses} {...rest}>
{children}
</EllipsisContainer>
</div>
)
}
const multiLineEllipsis = css<{ $maxLine: number }>`
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: ${({ $maxLine }) => $maxLine};
overflow-wrap: break-word;
`
const singleLineEllipsis = css`
display: block;
white-space: nowrap;
`
const EllipsisContainer = styled.div<{ $maxLine: number }>`
overflow: hidden;
text-overflow: ellipsis;
${({ $maxLine }) => ($maxLine > 1 ? multiLineEllipsis : singleLineEllipsis)}
`
export default Ellipsis

View File

@ -1,7 +1,8 @@
// Original path: src/renderer/src/components/ListItem/index.tsx
import { Typography } from 'antd'
import { Tooltip } from '@heroui/react'
import { ReactNode } from 'react'
import styled from 'styled-components'
import { cn } from '../../../utils'
interface ListItemProps {
active?: boolean
@ -12,81 +13,49 @@ interface ListItemProps {
onClick?: () => void
rightContent?: ReactNode
style?: React.CSSProperties
className?: string
ref?: React.Ref<HTMLDivElement>
}
const ListItem = ({ active, icon, title, subtitle, titleStyle, onClick, rightContent, style }: ListItemProps) => {
const ListItem = ({
active,
icon,
title,
subtitle,
titleStyle,
onClick,
rightContent,
style,
className,
ref
}: ListItemProps) => {
return (
<ListItemContainer className={active ? 'active' : ''} onClick={onClick} style={style}>
<ListItemContent>
{icon && <IconWrapper>{icon}</IconWrapper>}
<TextContainer>
<Typography.Text style={titleStyle} ellipsis={{ expanded: false, tooltip: title }}>
{title}
</Typography.Text>
{subtitle && <SubtitleText>{subtitle}</SubtitleText>}
</TextContainer>
{rightContent && <RightContentWrapper>{rightContent}</RightContentWrapper>}
</ListItemContent>
</ListItemContainer>
<div
ref={ref}
className={cn(
'px-3 py-1.5 rounded-md text-xs flex flex-col justify-between relative cursor-pointer border border-transparent',
'hover:bg-gray-50 dark:hover:bg-gray-800',
active && 'bg-gray-50 dark:bg-gray-800 border-gray-200 dark:border-gray-700',
className
)}
onClick={onClick}
style={style}>
<div className="flex items-center gap-0.5 overflow-hidden text-xs">
{icon && <span className="flex items-center justify-center mr-2">{icon}</span>}
<div className="flex-1 flex flex-col overflow-hidden">
<Tooltip content={title} placement="top">
<div className="truncate text-gray-900 dark:text-gray-100" style={titleStyle}>
{title}
</div>
</Tooltip>
{subtitle && (
<div className="text-[10px] text-gray-500 dark:text-gray-400 mt-0.5 line-clamp-1">{subtitle}</div>
)}
</div>
{rightContent && <div className="ml-auto">{rightContent}</div>}
</div>
</div>
)
}
const ListItemContainer = styled.div`
padding: 7px 12px;
border-radius: var(--list-item-border-radius);
font-size: 13px;
display: flex;
flex-direction: column;
justify-content: space-between;
position: relative;
cursor: pointer;
border: 1px solid transparent;
&:hover {
background-color: var(--color-background-soft);
}
&.active {
background-color: var(--color-background-soft);
border: 1px solid var(--color-border-soft);
}
`
const ListItemContent = styled.div`
display: flex;
align-items: center;
gap: 2px;
overflow: hidden;
font-size: 13px;
`
const IconWrapper = styled.span`
display: flex;
align-items: center;
justify-content: center;
margin-right: 8px;
`
const TextContainer = styled.div`
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
`
const SubtitleText = styled.div`
font-size: 10px;
color: var(--color-text-soft);
margin-top: 2px;
display: -webkit-box;
-webkit-line-clamp: 1;
-webkit-box-orient: vertical;
overflow: hidden;
color: var(--color-text-3);
`
const RightContentWrapper = styled.div`
margin-left: auto;
`
export default ListItem

View File

@ -8,12 +8,16 @@ type Props = {
maxContext: number
style?: CSSProperties
size?: number
className?: string
ref?: React.Ref<HTMLSpanElement>
}
export default function MaxContextCount({ maxContext, style, size = 14 }: Props) {
export default function MaxContextCount({ maxContext, style, size = 14, className, ref }: Props) {
return maxContext === MAX_CONTEXT_COUNT ? (
<InfinityIcon size={size} style={style} aria-label="infinity" />
<InfinityIcon size={size} style={style} className={className} aria-label="infinity" />
) : (
<span style={style}>{maxContext.toString()}</span>
<span ref={ref} style={style} className={className}>
{maxContext.toString()}
</span>
)
}

View File

@ -3,8 +3,8 @@ import { isEqual } from 'lodash'
import { ChevronRight, Lightbulb } from 'lucide-react'
import { motion } from 'motion/react'
import React, { useEffect, useMemo, useState } from 'react'
import styled from 'styled-components'
import { cn } from '../../../utils'
import { lightbulbVariants } from './defaultVariants'
interface ThinkingEffectProps {
@ -12,11 +12,19 @@ interface ThinkingEffectProps {
thinkingTimeText: React.ReactNode
content: string
expanded: boolean
className?: string
ref?: React.Ref<HTMLDivElement>
}
const ThinkingEffect: React.FC<ThinkingEffectProps> = ({ isThinking, thinkingTimeText, content, expanded }) => {
const ThinkingEffect: React.FC<ThinkingEffectProps> = ({
isThinking,
thinkingTimeText,
content,
expanded,
className,
ref
}) => {
const [messages, setMessages] = useState<string[]>([])
useEffect(() => {
const allLines = (content || '').split('\n')
const newMessages = isThinking ? allLines.slice(0, -1) : allLines
@ -39,22 +47,44 @@ const ThinkingEffect: React.FC<ThinkingEffectProps> = ({ isThinking, thinkingTim
}, [showThinking, messages.length])
return (
<ThinkingContainer style={{ height: containerHeight }} className={expanded ? 'expanded' : ''}>
<LoadingContainer>
<motion.div variants={lightbulbVariants} animate={isThinking ? 'active' : 'idle'} initial="idle">
<div
ref={ref}
style={{ height: containerHeight }}
className={cn(
'w-full rounded-xl overflow-hidden relative flex items-center border-0.5 border-gray-200 dark:border-gray-700 transition-all duration-150 pointer-events-none select-none',
expanded && 'rounded-b-none',
className
)}>
<div className="w-12 flex justify-center items-center h-full flex-shrink-0 relative pl-1.5 transition-all duration-150">
<motion.div
variants={lightbulbVariants}
animate={isThinking ? 'active' : 'idle'}
initial="idle"
className="flex justify-center items-center">
<Lightbulb
size={!showThinking || messages.length < 2 ? 20 : 30}
style={{ transition: 'width,height, 150ms' }}
/>
</motion.div>
</LoadingContainer>
</div>
<TextContainer>
<Title className={!showThinking || !messages.length ? 'showThinking' : ''}>{thinkingTimeText}</Title>
<div className="flex-1 h-full py-1.5 overflow-hidden relative">
<div
className={cn(
'absolute inset-x-0 top-0 text-sm leading-3.5 font-medium py-2.5 z-50 transition-all duration-150',
(!showThinking || !messages.length) && 'pt-3'
)}>
{thinkingTimeText}
</div>
{showThinking && (
<Content>
<Messages
<div
className="w-full h-full relative"
style={{
mask: 'linear-gradient(to bottom, rgb(0 0 0 / 0%) 0%, rgb(0 0 0 / 0%) 35%, rgb(0 0 0 / 25%) 40%, rgb(0 0 0 / 100%) 90%, rgb(0 0 0 / 100%) 100%)'
}}>
<motion.div
className="w-full absolute top-full flex flex-col justify-end"
style={{
height: messages.length * LINE_HEIGHT
}}
@ -71,120 +101,28 @@ const ThinkingEffect: React.FC<ThinkingEffectProps> = ({ isThinking, thinkingTim
{messages.map((message, index) => {
if (index < messages.length - 5) return null
return <Message key={index}>{message}</Message>
return (
<div
key={index}
className="w-full leading-3.5 text-xs text-gray-600 dark:text-gray-300 whitespace-nowrap overflow-hidden text-ellipsis">
{message}
</div>
)
})}
</Messages>
</Content>
</motion.div>
</div>
)}
</TextContainer>
<ArrowContainer className={expanded ? 'expanded' : ''}>
<ChevronRight size={20} color="var(--color-text-3)" strokeWidth={1} />
</ArrowContainer>
</ThinkingContainer>
</div>
<div
className={cn(
'w-10 flex justify-center items-center h-full flex-shrink-0 relative text-gray-400 dark:text-gray-500 transition-transform duration-150',
expanded && 'rotate-90'
)}>
<ChevronRight size={20} strokeWidth={1} />
</div>
</div>
)
}
const ThinkingContainer = styled.div`
width: 100%;
border-radius: 10px;
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: 10px 10px 0 0;
}
`
const Title = styled.div`
position: absolute;
inset: 0 0 auto 0;
font-size: 14px;
line-height: 14px;
font-weight: 500;
padding: 10px 0;
z-index: 99;
transition: padding-top 150ms;
&.showThinking {
padding-top: 12px;
}
`
const LoadingContainer = styled.div`
width: 50px;
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;
}
`
const TextContainer = styled.div`
flex: 1;
height: 100%;
padding: 5px 0;
overflow: hidden;
position: relative;
`
const Content = styled.div`
width: 100%;
height: 100%;
mask: linear-gradient(
to bottom,
rgb(0 0 0 / 0%) 0%,
rgb(0 0 0 / 0%) 35%,
rgb(0 0 0 / 25%) 40%,
rgb(0 0 0 / 100%) 90%,
rgb(0 0 0 / 100%) 100%
);
position: relative;
`
const Messages = styled(motion.div)`
width: 100%;
position: absolute;
top: 100%;
display: flex;
flex-direction: column;
justify-content: flex-end;
`
const Message = styled.div`
width: 100%;
line-height: 14px;
font-size: 11px;
color: var(--color-text-2);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
`
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

View File

@ -0,0 +1,167 @@
import type { Meta, StoryObj } from '@storybook/react'
import Ellipsis from '../../../src/components/display/Ellipsis'
const meta = {
title: 'Display/Ellipsis',
component: Ellipsis,
parameters: {
layout: 'centered',
docs: {
description: {
component: '一个用于显示省略文本的组件,支持单行和多行省略功能。'
}
}
},
tags: ['autodocs'],
argTypes: {
maxLine: {
control: { type: 'number' },
description: '最大显示行数默认为1。设置为1时为单行省略大于1时为多行省略。'
},
className: {
control: { type: 'text' },
description: '自定义 CSS 类名'
},
children: {
control: { type: 'text' },
description: '要显示的文本内容'
}
},
args: {
children: '这是一段很长的文本内容,用于演示省略功能的效果。当文本超出容器宽度或高度时,会自动显示省略号。'
}
} satisfies Meta<typeof Ellipsis>
export default meta
type Story = StoryObj<typeof meta>
// 默认单行省略
export const Default: Story = {
args: {
maxLine: 1
},
render: (args) => (
<div className="w-60 p-4 border border-gray-200 dark:border-gray-700 rounded">
<Ellipsis {...args} />
</div>
)
}
// 多行省略
export const MultiLine: Story = {
args: {
maxLine: 3,
children:
'这是一段很长的文本内容,用于演示多行省略功能的效果。当文本内容超过指定的最大行数时,会在最后一行的末尾显示省略号。这个功能特别适用于显示文章摘要、商品描述等需要限制显示行数的场景。'
},
render: (args) => (
<div className="w-80 p-4 border border-gray-200 dark:border-gray-700 rounded">
<Ellipsis {...args} />
</div>
)
}
// 不同的最大行数
export const DifferentMaxLines: Story = {
render: () => (
<div className="space-y-4 max-w-lg">
<div>
<h3 className="text-sm font-medium mb-2"> (maxLine = 1)</h3>
<div className="p-3 border border-gray-200 dark:border-gray-700 rounded">
<Ellipsis maxLine={1}></Ellipsis>
</div>
</div>
<div>
<h3 className="text-sm font-medium mb-2"> (maxLine = 2)</h3>
<div className="p-3 border border-gray-200 dark:border-gray-700 rounded">
<Ellipsis maxLine={2}>
</Ellipsis>
</div>
</div>
<div>
<h3 className="text-sm font-medium mb-2"> (maxLine = 3)</h3>
<div className="p-3 border border-gray-200 dark:border-gray-700 rounded">
<Ellipsis maxLine={3}>
</Ellipsis>
</div>
</div>
</div>
)
}
// 短文本(不需要省略)
export const ShortText: Story = {
args: {
maxLine: 2,
children: '这是一段短文本。'
},
render: (args) => (
<div className="w-80 p-4 border border-gray-200 dark:border-gray-700 rounded">
<Ellipsis {...args} />
</div>
)
}
// 自定义样式
export const CustomStyle: Story = {
args: {
maxLine: 2,
className: 'text-blue-600 font-medium text-lg',
children: '这是一段带有自定义样式的长文本内容,用于演示如何自定义省略文本的样式。'
},
render: (args) => (
<div className="w-80 p-4 border border-gray-200 dark:border-gray-700 rounded">
<Ellipsis {...args} />
</div>
)
}
// 不同容器宽度的响应式展示
export const ResponsiveWidth: Story = {
render: () => (
<div className="space-y-4">
<div>
<h3 className="text-sm font-medium mb-2"> (200px)</h3>
<div className="w-50 p-3 border border-gray-200 dark:border-gray-700 rounded">
<Ellipsis maxLine={2}></Ellipsis>
</div>
</div>
<div>
<h3 className="text-sm font-medium mb-2"> (300px)</h3>
<div className="w-75 p-3 border border-gray-200 dark:border-gray-700 rounded">
<Ellipsis maxLine={2}></Ellipsis>
</div>
</div>
<div>
<h3 className="text-sm font-medium mb-2"> (400px)</h3>
<div className="w-100 p-3 border border-gray-200 dark:border-gray-700 rounded">
<Ellipsis maxLine={2}></Ellipsis>
</div>
</div>
</div>
)
}
// 包含HTML内容
export const WithHTMLContent: Story = {
args: {
maxLine: 2
},
render: (args) => (
<div className="w-80 p-4 border border-gray-200 dark:border-gray-700 rounded">
<Ellipsis {...args}>
<span className="text-red-500"></span><strong className="font-bold"></strong>
<em className="italic"></em>
HTML元素的省略效果
</Ellipsis>
</div>
)
}

View File

@ -0,0 +1,327 @@
import { Button } from '@heroui/react'
import type { Meta, StoryObj } from '@storybook/react'
import {
ChevronRight,
Edit,
File,
Folder,
Heart,
Mail,
MoreHorizontal,
Phone,
Settings,
Star,
Trash2,
User
} from 'lucide-react'
import { action } from 'storybook/actions'
import ListItem from '../../../src/components/display/ListItem'
const meta: Meta<typeof ListItem> = {
title: 'Display/ListItem',
component: ListItem,
parameters: {
layout: 'centered',
docs: {
description: {
component: '一个通用的列表项组件,支持图标、标题、副标题、激活状态和右侧内容等功能。'
}
}
},
tags: ['autodocs'],
argTypes: {
active: {
control: { type: 'boolean' },
description: '是否处于激活状态,激活时会显示高亮样式'
},
icon: {
control: { type: 'text' },
description: '左侧图标,可以是任何 React 节点'
},
title: {
control: { type: 'text' },
description: '标题内容,必填字段,可以是文本或 React 节点'
},
subtitle: {
control: { type: 'text' },
description: '副标题内容,显示在标题下方'
},
titleStyle: {
control: { type: 'object' },
description: '标题的自定义样式对象'
},
onClick: {
action: 'clicked',
description: '点击事件处理函数'
},
rightContent: {
control: { type: 'text' },
description: '右侧内容,可以是任何 React 节点'
},
className: {
control: { type: 'text' },
description: '自定义 CSS 类名'
}
},
args: {
title: '列表项标题',
onClick: action('clicked')
}
} satisfies Meta<typeof ListItem>
export default meta
type Story = StoryObj<typeof meta>
// 默认样式
export const Default: Story = {
args: {
title: '默认列表项'
},
render: (args) => (
<div className="w-80">
<ListItem {...args} />
</div>
)
}
// 带图标
export const WithIcon: Story = {
args: {
icon: <File size={16} />,
title: '带图标的列表项',
subtitle: '这是一个副标题'
},
render: (args) => (
<div className="w-80">
<ListItem {...args} />
</div>
)
}
// 激活状态
export const Active: Story = {
args: {
icon: <Folder size={16} />,
title: '激活状态的列表项',
subtitle: '当前选中项',
active: true
},
render: (args) => (
<div className="w-80">
<ListItem {...args} />
</div>
)
}
// 带右侧内容
export const WithRightContent: Story = {
args: {
icon: <Star size={16} />,
title: '带右侧内容的列表项',
subtitle: '右侧有附加信息',
rightContent: <ChevronRight size={16} className="text-gray-400" />
},
render: (args) => (
<div className="w-80">
<ListItem {...args} />
</div>
)
}
// 多种图标类型
export const DifferentIcons: Story = {
render: () => (
<div className="w-80 space-y-2">
<ListItem
icon={<File size={16} className="text-blue-500" />}
title="文件项"
subtitle="文档文件"
onClick={action('file-clicked')}
/>
<ListItem
icon={<Folder size={16} className="text-yellow-500" />}
title="文件夹项"
subtitle="目录文件夹"
onClick={action('folder-clicked')}
/>
<ListItem
icon={<User size={16} className="text-green-500" />}
title="用户项"
subtitle="用户信息"
onClick={action('user-clicked')}
/>
<ListItem
icon={<Settings size={16} className="text-gray-500" />}
title="设置项"
subtitle="系统设置"
onClick={action('settings-clicked')}
/>
</div>
)
}
// 不同长度的标题和副标题
export const DifferentContentLength: Story = {
render: () => (
<div className="w-80 space-y-2">
<ListItem icon={<Mail size={16} />} title="短标题" subtitle="短副标题" />
<ListItem
icon={<Phone size={16} />}
title="这是一个比较长的标题,可能会被截断"
subtitle="这也是一个比较长的副标题,用于测试文本溢出效果"
/>
<ListItem
icon={<Heart size={16} />}
title="超级长的标题内容用于测试文本省略功能,当标题过长时会自动截断并显示省略号"
subtitle="超级长的副标题内容用于测试文本省略功能,当副标题过长时也会自动截断"
/>
</div>
)
}
// 不同的右侧内容类型
export const DifferentRightContent: Story = {
render: () => (
<div className="w-80 space-y-2">
<ListItem
icon={<File size={16} />}
title="带箭头"
subtitle="导航类型"
rightContent={<ChevronRight size={16} className="text-gray-400" />}
/>
<ListItem
icon={<Folder size={16} />}
title="带按钮"
subtitle="操作类型"
rightContent={
<Button size="sm" variant="ghost" isIconOnly>
<MoreHorizontal size={16} />
</Button>
}
/>
<ListItem
icon={<User size={16} />}
title="带文本"
subtitle="信息显示"
rightContent={<span className="text-xs text-gray-500">线</span>}
/>
<ListItem
icon={<Settings size={16} />}
title="带多个操作"
subtitle="复合操作"
rightContent={
<div className="flex gap-1">
<Button size="sm" variant="ghost" isIconOnly>
<Edit size={14} />
</Button>
<Button size="sm" variant="ghost" isIconOnly color="danger">
<Trash2 size={14} />
</Button>
</div>
}
/>
</div>
)
}
// 激活状态对比
export const ActiveComparison: Story = {
render: () => (
<div className="w-80 space-y-2">
<h3 className="text-sm font-medium mb-2"></h3>
<ListItem
icon={<File size={16} />}
title="普通列表项"
subtitle="未激活状态"
rightContent={<ChevronRight size={16} className="text-gray-400" />}
/>
<h3 className="text-sm font-medium mb-2 mt-4"></h3>
<ListItem
icon={<File size={16} />}
title="激活列表项"
subtitle="当前选中状态"
active={true}
rightContent={<ChevronRight size={16} className="text-gray-400" />}
/>
</div>
)
}
// 自定义标题样式
export const CustomTitleStyle: Story = {
render: () => (
<div className="w-80 space-y-2">
<ListItem
icon={<Star size={16} />}
title="红色标题"
subtitle="自定义颜色"
titleStyle={{ color: '#ef4444', fontWeight: 'bold' }}
/>
<ListItem
icon={<Heart size={16} />}
title="大号标题"
subtitle="自定义大小"
titleStyle={{ fontSize: '16px', fontWeight: '600' }}
/>
<ListItem
icon={<User size={16} />}
title="斜体标题"
subtitle="自定义样式"
titleStyle={{ fontStyle: 'italic', color: '#6366f1' }}
/>
</div>
)
}
// 无副标题
export const WithoutSubtitle: Story = {
render: () => (
<div className="w-80 space-y-2">
<ListItem icon={<File size={16} />} title="只有标题的列表项" />
<ListItem
icon={<Folder size={16} />}
title="另一个只有标题的项"
rightContent={<ChevronRight size={16} className="text-gray-400" />}
/>
</div>
)
}
// 无图标
export const WithoutIcon: Story = {
render: () => (
<div className="w-80 space-y-2">
<ListItem title="无图标列表项" subtitle="没有左侧图标" />
<ListItem
title="另一个无图标项"
subtitle="简洁样式"
rightContent={<span className="text-xs text-gray-500"></span>}
/>
</div>
)
}
// 完整功能展示
export const FullFeatures: Story = {
render: () => (
<div className="w-80 space-y-2">
<ListItem
icon={<File size={16} className="text-blue-500" />}
title="完整功能展示"
subtitle="包含所有功能的列表项"
titleStyle={{ fontWeight: '600' }}
active={true}
rightContent={
<div className="flex items-center gap-2">
<span className="text-xs text-gray-500">NEW</span>
<ChevronRight size={16} className="text-gray-400" />
</div>
}
onClick={action('full-features-clicked')}
className="hover:shadow-sm transition-shadow"
/>
</div>
)
}

View File

@ -0,0 +1,299 @@
import type { Meta, StoryObj } from '@storybook/react'
import MaxContextCount from '../../../src/components/display/MaxContextCount'
const meta: Meta<typeof MaxContextCount> = {
title: 'Display/MaxContextCount',
component: MaxContextCount,
parameters: {
layout: 'centered',
docs: {
description: {
component: '一个用于显示最大上下文数量的组件。当数量达到100时显示无限符号否则显示具体数字。'
}
}
},
tags: ['autodocs'],
argTypes: {
maxContext: {
control: { type: 'number', min: 0, max: 100, step: 1 },
description: '最大上下文数量。当值为100时显示无限符号(∞),其他值显示具体数字。'
},
size: {
control: { type: 'number', min: 8, max: 48, step: 2 },
description: '图标大小默认为14像素'
},
className: {
control: { type: 'text' },
description: '自定义 CSS 类名'
},
style: {
control: { type: 'object' },
description: '自定义样式对象'
}
},
args: {
maxContext: 10,
size: 14
}
} satisfies Meta<typeof MaxContextCount>
export default meta
type Story = StoryObj<typeof meta>
// 默认数字显示
export const Default: Story = {
args: {
maxContext: 10
},
render: (args) => (
<div className="p-4 border border-gray-200 dark:border-gray-700 rounded inline-flex items-center gap-2">
<span className="text-sm"></span>
<MaxContextCount {...args} />
</div>
)
}
// 无限符号显示
export const InfinitySymbol: Story = {
args: {
maxContext: 100
},
render: (args) => (
<div className="p-4 border border-gray-200 dark:border-gray-700 rounded inline-flex items-center gap-2">
<span className="text-sm"></span>
<MaxContextCount {...args} />
</div>
)
}
// 不同的数值范围
export const DifferentValues: Story = {
render: () => (
<div className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div className="p-3 border border-gray-200 dark:border-gray-700 rounded">
<div className="text-xs text-gray-500 mb-2"></div>
<div className="flex items-center gap-2">
<span className="text-sm"></span>
<MaxContextCount maxContext={5} />
</div>
</div>
<div className="p-3 border border-gray-200 dark:border-gray-700 rounded">
<div className="text-xs text-gray-500 mb-2"></div>
<div className="flex items-center gap-2">
<span className="text-sm"></span>
<MaxContextCount maxContext={25} />
</div>
</div>
<div className="p-3 border border-gray-200 dark:border-gray-700 rounded">
<div className="text-xs text-gray-500 mb-2"></div>
<div className="flex items-center gap-2">
<span className="text-sm"></span>
<MaxContextCount maxContext={99} />
</div>
</div>
<div className="p-3 border border-gray-200 dark:border-gray-700 rounded">
<div className="text-xs text-gray-500 mb-2"></div>
<div className="flex items-center gap-2">
<span className="text-sm"></span>
<MaxContextCount maxContext={100} />
</div>
</div>
</div>
</div>
)
}
// 不同大小
export const DifferentSizes: Story = {
render: () => (
<div className="space-y-4">
<div className="flex items-center gap-4">
<div className="flex items-center gap-2">
<span className="text-xs"> (12px)</span>
<MaxContextCount maxContext={20} size={12} />
</div>
</div>
<div className="flex items-center gap-4">
<div className="flex items-center gap-2">
<span className="text-sm"> (14px)</span>
<MaxContextCount maxContext={50} size={14} />
</div>
</div>
<div className="flex items-center gap-4">
<div className="flex items-center gap-2">
<span className="text-base"> (18px)</span>
<MaxContextCount maxContext={75} size={18} />
</div>
</div>
<div className="flex items-center gap-4">
<div className="flex items-center gap-2">
<span className="text-lg"> (24px)</span>
<MaxContextCount maxContext={100} size={24} />
</div>
</div>
</div>
)
}
// 无限符号不同大小对比
export const InfinityDifferentSizes: Story = {
render: () => (
<div className="space-y-4">
<h3 className="text-sm font-medium"></h3>
<div className="flex items-center gap-6">
<div className="flex items-center gap-2">
<span className="text-xs">12px</span>
<MaxContextCount maxContext={100} size={12} />
</div>
<div className="flex items-center gap-2">
<span className="text-sm">16px</span>
<MaxContextCount maxContext={100} size={16} />
</div>
<div className="flex items-center gap-2">
<span className="text-base">20px</span>
<MaxContextCount maxContext={100} size={20} />
</div>
<div className="flex items-center gap-2">
<span className="text-lg">28px</span>
<MaxContextCount maxContext={100} size={28} />
</div>
</div>
</div>
)
}
// 自定义样式
export const CustomStyles: Story = {
render: () => (
<div className="space-y-4">
<div className="flex items-center gap-4">
<span className="text-sm"></span>
<MaxContextCount maxContext={42} style={{ color: '#ef4444', fontWeight: 'bold' }} />
</div>
<div className="flex items-center gap-4">
<span className="text-sm"></span>
<MaxContextCount maxContext={100} size={18} style={{ color: '#3b82f6' }} />
</div>
<div className="flex items-center gap-4">
<span className="text-sm"></span>
<MaxContextCount
maxContext={88}
className="px-2 py-1 bg-green-100 dark:bg-green-900 text-green-800 dark:text-green-200 rounded-full text-xs font-medium"
/>
</div>
<div className="flex items-center gap-4">
<span className="text-sm"></span>
<MaxContextCount
maxContext={100}
size={16}
className="p-1 border-2 border-purple-300 rounded-full text-purple-600"
/>
</div>
</div>
)
}
// 在实际使用场景中的展示
export const InRealScenarios: Story = {
render: () => (
<div className="space-y-4 max-w-md">
<div className="p-4 bg-gray-50 dark:bg-gray-800 rounded-lg">
<div className="flex items-center justify-between mb-2">
<span className="font-medium text-sm">AI </span>
</div>
<div className="space-y-2 text-sm">
<div className="flex justify-between">
<span className="text-gray-600 dark:text-gray-400"></span>
<span>GPT-4</span>
</div>
<div className="flex justify-between">
<span className="text-gray-600 dark:text-gray-400"></span>
<span>0.7</span>
</div>
<div className="flex justify-between items-center">
<span className="text-gray-600 dark:text-gray-400"></span>
<MaxContextCount maxContext={100} />
</div>
</div>
</div>
<div className="p-4 bg-gray-50 dark:bg-gray-800 rounded-lg">
<div className="flex items-center justify-between mb-2">
<span className="font-medium text-sm"></span>
</div>
<div className="space-y-2 text-sm">
<div className="flex justify-between items-center">
<span className="text-gray-600 dark:text-gray-400"></span>
<MaxContextCount maxContext={50} size={13} />
</div>
<div className="flex justify-between items-center">
<span className="text-gray-600 dark:text-gray-400"></span>
<MaxContextCount maxContext={20} size={13} />
</div>
</div>
</div>
</div>
)
}
// 边界值测试
export const EdgeCases: Story = {
render: () => (
<div className="space-y-4">
<h3 className="text-sm font-medium"></h3>
<div className="grid grid-cols-3 gap-4">
<div className="p-3 border border-gray-200 dark:border-gray-700 rounded text-center">
<div className="text-xs text-gray-500 mb-2"></div>
<MaxContextCount maxContext={0} />
</div>
<div className="p-3 border border-gray-200 dark:border-gray-700 rounded text-center">
<div className="text-xs text-gray-500 mb-2"> 99</div>
<MaxContextCount maxContext={99} />
</div>
<div className="p-3 border border-gray-200 dark:border-gray-700 rounded text-center">
<div className="text-xs text-gray-500 mb-2"> 100</div>
<MaxContextCount maxContext={100} />
</div>
</div>
</div>
)
}
// 深色主题下的表现
export const DarkTheme: Story = {
parameters: {
backgrounds: { default: 'dark' }
},
render: () => (
<div className="space-y-4 p-4 bg-gray-900 rounded-lg">
<h3 className="text-sm font-medium text-white"></h3>
<div className="space-y-3">
<div className="flex items-center gap-4 text-gray-300">
<span className="text-sm"></span>
<MaxContextCount maxContext={30} />
</div>
<div className="flex items-center gap-4 text-gray-300">
<span className="text-sm"></span>
<MaxContextCount maxContext={100} size={16} />
</div>
<div className="flex items-center gap-4 text-gray-300">
<span className="text-sm"></span>
<MaxContextCount maxContext={100} size={18} style={{ color: '#60a5fa' }} />
</div>
</div>
</div>
)
}

View File

@ -0,0 +1,399 @@
import { Button } from '@heroui/react'
import type { Meta, StoryObj } from '@storybook/react'
import { useEffect, useMemo, useState } from 'react'
import ThinkingEffect from '../../../src/components/display/ThinkingEffect'
const meta: Meta<typeof ThinkingEffect> = {
title: 'Display/ThinkingEffect',
component: ThinkingEffect,
parameters: {
layout: 'centered',
docs: {
description: {
component: '一个用于显示AI思考过程的动画组件包含灯泡动画、思考内容滚动展示和展开收缩功能。'
}
}
},
tags: ['autodocs'],
argTypes: {
isThinking: {
control: { type: 'boolean' },
description: '是否正在思考,控制动画状态和内容显示'
},
thinkingTimeText: {
control: { type: 'text' },
description: '思考时间文本,显示在组件顶部'
},
content: {
control: { type: 'text' },
description: '思考内容,多行文本用换行符分隔,最后一行在思考时会被过滤'
},
expanded: {
control: { type: 'boolean' },
description: '是否展开状态,影响组件的显示样式'
},
className: {
control: { type: 'text' },
description: '自定义 CSS 类名'
}
},
args: {
isThinking: true,
thinkingTimeText: '思考中...',
content: `正在分析问题\n寻找最佳解决方案\n整理思路和逻辑\n准备回答`,
expanded: false
}
} satisfies Meta<typeof ThinkingEffect>
export default meta
type Story = StoryObj<typeof meta>
// 默认思考状态
export const Default: Story = {
args: {
isThinking: true,
thinkingTimeText: '思考中 2s',
content: `正在分析用户的问题\n查找相关信息\n整理回答思路`,
expanded: false
},
render: (args) => (
<div className="w-96">
<ThinkingEffect {...args} />
</div>
)
}
// 非思考状态(静止)
export const NotThinking: Story = {
args: {
isThinking: false,
thinkingTimeText: '思考完成',
content: `已完成思考\n找到最佳答案\n准备响应`,
expanded: false
},
render: (args) => (
<div className="w-96">
<ThinkingEffect {...args} />
</div>
)
}
// 展开状态
export const Expanded: Story = {
args: {
isThinking: false,
thinkingTimeText: '思考用时 5s',
content: `第一步:理解问题本质\n第二步分析可能的解决方案\n第三步评估各方案的优缺点\n第四步选择最优方案\n第五步构建详细回答`,
expanded: true
},
render: (args) => (
<div className="w-96">
<ThinkingEffect {...args} />
</div>
)
}
// 交互式演示
export const Interactive: Story = {
render: function Render() {
const [isThinking, setIsThinking] = useState(false)
const [expanded, setExpanded] = useState(false)
const [thinkingTime, setThinkingTime] = useState(0)
const thinkingSteps = useMemo(() => {
return [
'开始分析问题...',
'查找相关资料和信息',
'对比不同的解决方案',
'评估方案的可行性',
'选择最佳解决路径',
'构建完整的回答框架',
'检查逻辑的连贯性',
'优化回答的表达方式'
]
}, [])
const [content, setContent] = useState('')
useEffect(() => {
let interval: NodeJS.Timeout
if (isThinking) {
setThinkingTime(0)
setContent(thinkingSteps[0])
interval = setInterval(() => {
setThinkingTime((prev) => {
const newTime = prev + 1
const stepIndex = Math.min(Math.floor(newTime / 2), thinkingSteps.length - 1)
const currentSteps = thinkingSteps.slice(0, stepIndex + 1)
setContent(currentSteps.join('\n'))
return newTime
})
}, 1000)
}
return () => {
if (interval) clearInterval(interval)
}
}, [isThinking, thinkingSteps])
const handleStartThinking = () => {
setIsThinking(true)
setExpanded(false)
}
const handleStopThinking = () => {
setIsThinking(false)
}
const handleToggleExpanded = () => {
setExpanded(!expanded)
}
return (
<div className="w-96 space-y-4">
<div className="flex gap-2 flex-wrap">
<Button size="sm" color="primary" onClick={handleStartThinking} disabled={isThinking}>
</Button>
<Button size="sm" color="secondary" onClick={handleStopThinking} disabled={!isThinking}>
</Button>
<Button size="sm" variant="ghost" onClick={handleToggleExpanded}>
{expanded ? '收起' : '展开'}
</Button>
</div>
<ThinkingEffect
isThinking={isThinking}
thinkingTimeText={isThinking ? `思考中 ${thinkingTime}s` : `思考完成 ${thinkingTime}s`}
content={content}
expanded={expanded}
/>
</div>
)
}
}
// 不同内容长度
export const DifferentContentLength: Story = {
render: () => (
<div className="space-y-6 w-96">
<div>
<h3 className="text-sm font-medium mb-2"></h3>
<ThinkingEffect isThinking thinkingTimeText="思考中 1s" content={`分析问题\n寻找答案`} expanded={false} />
</div>
<div>
<h3 className="text-sm font-medium mb-2"></h3>
<ThinkingEffect
isThinking
thinkingTimeText="思考中 3s"
content={`第一步:理解问题\n第二步分析背景\n第三步寻找解决方案\n第四步验证方案可行性\n第五步准备详细回答`}
expanded={false}
/>
</div>
<div>
<h3 className="text-sm font-medium mb-2"></h3>
<ThinkingEffect
isThinking
thinkingTimeText="思考中 8s"
content={`开始分析用户提出的复杂问题\n识别问题的核心要素和关键词\n搜索相关的知识领域和概念\n整理可能的解决思路和方法\n评估不同方案的优缺点\n考虑实际应用中的限制条件\n构建逻辑清晰的回答框架\n检查答案的完整性和准确性\n优化语言表达的清晰度`}
expanded={false}
/>
</div>
</div>
)
}
// 不同的思考时间文本
export const DifferentThinkingTime: Story = {
render: () => (
<div className="space-y-4 w-96">
<ThinkingEffect
isThinking
thinkingTimeText="思考中..."
content={`正在处理问题\n分析可能的答案`}
expanded={false}
/>
<ThinkingEffect
isThinking
thinkingTimeText="深度思考中 10s"
content={`进行复杂分析\n考虑多种可能性`}
expanded={false}
/>
<ThinkingEffect
isThinking={false}
thinkingTimeText="🎯 思考完成 (用时 15s)"
content={`问题分析完毕\n答案已准备就绪`}
expanded={false}
/>
<ThinkingEffect
isThinking={false}
thinkingTimeText={
<div className="flex items-center gap-2">
<span className="text-green-600 text-xs"></span>
<span></span>
</div>
}
content={`成功找到解决方案\n可以开始回答`}
expanded={false}
/>
</div>
)
}
// 空内容状态
export const EmptyContent: Story = {
render: () => (
<div className="space-y-4 w-96">
<div>
<h3 className="text-sm font-medium mb-2"> - </h3>
<ThinkingEffect isThinking thinkingTimeText="准备开始思考..." content="" expanded={false} />
</div>
<div>
<h3 className="text-sm font-medium mb-2"> - </h3>
<ThinkingEffect isThinking={false} thinkingTimeText="等待输入" content="" expanded={false} />
</div>
</div>
)
}
// 实时内容更新演示
export const RealTimeUpdate: Story = {
render: function Render() {
const [content, setContent] = useState('')
const [isThinking, setIsThinking] = useState(false)
const [step, setStep] = useState(0)
const steps = useMemo(() => {
return [
'开始分析问题的复杂性...',
'识别关键信息和要求',
'搜索相关的知识点',
'整理可能的解决思路',
'评估不同方案的优缺点',
'选择最优的解决方案',
'构建详细的回答框架',
'检查逻辑的连贯性',
'优化表达的清晰度',
'完成最终答案的准备'
]
}, [])
useEffect(() => {
if (isThinking && step < steps.length) {
const timer = setTimeout(() => {
const newContent = steps.slice(0, step + 1).join('\n')
setContent(newContent)
setStep((prev) => prev + 1)
}, 1500)
return () => clearTimeout(timer)
} else if (step >= steps.length) {
setIsThinking(false)
}
return undefined
}, [isThinking, step, steps])
const handleStart = () => {
setIsThinking(true)
setStep(0)
setContent('')
}
const handleReset = () => {
setIsThinking(false)
setStep(0)
setContent('')
}
return (
<div className="w-96 space-y-4">
<div className="flex gap-2">
<Button size="sm" color="primary" onClick={handleStart} disabled={isThinking}>
</Button>
<Button size="sm" variant="ghost" onClick={handleReset}>
</Button>
</div>
<ThinkingEffect
isThinking={isThinking}
thinkingTimeText={isThinking ? `思考中... 步骤 ${step}/${steps.length}` : '思考完成'}
content={content}
expanded={false}
/>
</div>
)
}
}
// 自定义样式
export const CustomStyles: Story = {
render: () => (
<div className="space-y-4 w-96">
<div>
<h3 className="text-sm font-medium mb-2"></h3>
<ThinkingEffect
isThinking={true}
thinkingTimeText="自定义样式思考中..."
content="应用自定义样式\n测试视觉效果"
expanded={false}
className="border-blue-300 bg-blue-50 dark:bg-blue-950"
/>
</div>
<div>
<h3 className="text-sm font-medium mb-2"></h3>
<ThinkingEffect
isThinking={false}
thinkingTimeText="思考完成"
content="圆角和阴影效果\n增强视觉体验"
expanded={false}
className="rounded-2xl shadow-lg border-purple-300 bg-purple-50 dark:bg-purple-950"
/>
</div>
</div>
)
}
// 错误和边界情况
export const EdgeCases: Story = {
render: () => (
<div className="space-y-4 w-96">
<div>
<h3 className="text-sm font-medium mb-2"></h3>
<ThinkingEffect isThinking thinkingTimeText="思考中..." content="只有一行内容" expanded={false} />
</div>
<div>
<h3 className="text-sm font-medium mb-2"></h3>
<ThinkingEffect
isThinking
thinkingTimeText="处理长文本..."
content={`这是一行非常长的文本内容,用于测试组件在处理超长单行文本时的表现,看看是否能正确处理文本溢出和省略`}
expanded={false}
/>
</div>
<div>
<h3 className="text-sm font-medium mb-2"></h3>
<ThinkingEffect
isThinking
thinkingTimeText="特殊字符测试"
content={`包含特殊字符: @#$%^&*()_+\n中文、English、数字123\n换行\t制表符测试`}
expanded={false}
/>
</div>
</div>
)
}

View File

@ -13,7 +13,7 @@
"noFallthroughCasesInSwitch": true,
"outDir": "./dist",
"resolveJsonModule": true,
"rootDir": "./src",
"rootDir": ".",
"skipLibCheck": true,
"strict": true,
"target": "ES2020"

View File

@ -12,9 +12,6 @@ export default defineConfig({
clean: true,
dts: true,
tsconfig: 'tsconfig.json',
alias: {
'@shared': '../shared'
},
// 将 HeroUI、Tailwind 和其他 peer dependencies 标记为外部依赖
external: [
'react',