diff --git a/packages/ui/MIGRATION_STATUS.md b/packages/ui/MIGRATION_STATUS.md index ede78d008a..59c9ed0835 100644 --- a/packages/ui/MIGRATION_STATUS.md +++ b/packages/ui/MIGRATION_STATUS.md @@ -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 卡片 | diff --git a/packages/ui/MIGRATION_STATUS_EN.md b/packages/ui/MIGRATION_STATUS_EN.md index 36634b4532..4fe4471173 100644 --- a/packages/ui/MIGRATION_STATUS_EN.md +++ b/packages/ui/MIGRATION_STATUS_EN.md @@ -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 | diff --git a/packages/ui/src/components/display/Ellipsis/index.tsx b/packages/ui/src/components/display/Ellipsis/index.tsx index 45c60fdd40..c4c296079c 100644 --- a/packages/ui/src/components/display/Ellipsis/index.tsx +++ b/packages/ui/src/components/display/Ellipsis/index.tsx @@ -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 } & HTMLAttributes 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 ( - +
{children} - +
) } -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 diff --git a/packages/ui/src/components/display/ListItem/index.tsx b/packages/ui/src/components/display/ListItem/index.tsx index 344dfbadf0..228a09b039 100644 --- a/packages/ui/src/components/display/ListItem/index.tsx +++ b/packages/ui/src/components/display/ListItem/index.tsx @@ -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 } -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 ( - - - {icon && {icon}} - - - {title} - - {subtitle && {subtitle}} - - {rightContent && {rightContent}} - - +
+
+ {icon && {icon}} +
+ +
+ {title} +
+
+ {subtitle && ( +
{subtitle}
+ )} +
+ {rightContent &&
{rightContent}
} +
+
) } -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 diff --git a/packages/ui/src/components/display/MaxContextCount/index.tsx b/packages/ui/src/components/display/MaxContextCount/index.tsx index 0d874846f6..11a900b30e 100644 --- a/packages/ui/src/components/display/MaxContextCount/index.tsx +++ b/packages/ui/src/components/display/MaxContextCount/index.tsx @@ -8,12 +8,16 @@ type Props = { maxContext: number style?: CSSProperties size?: number + className?: string + ref?: React.Ref } -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 ? ( - + ) : ( - {maxContext.toString()} + + {maxContext.toString()} + ) } diff --git a/packages/ui/src/components/display/ThinkingEffect/index.tsx b/packages/ui/src/components/display/ThinkingEffect/index.tsx index 7ad0a1aef8..080496e922 100644 --- a/packages/ui/src/components/display/ThinkingEffect/index.tsx +++ b/packages/ui/src/components/display/ThinkingEffect/index.tsx @@ -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 } -const ThinkingEffect: React.FC = ({ isThinking, thinkingTimeText, content, expanded }) => { +const ThinkingEffect: React.FC = ({ + isThinking, + thinkingTimeText, + content, + expanded, + className, + ref +}) => { const [messages, setMessages] = useState([]) - useEffect(() => { const allLines = (content || '').split('\n') const newMessages = isThinking ? allLines.slice(0, -1) : allLines @@ -39,22 +47,44 @@ const ThinkingEffect: React.FC = ({ isThinking, thinkingTim }, [showThinking, messages.length]) return ( - - - +
+
+ - +
- - {thinkingTimeText} +
+
+ {thinkingTimeText} +
{showThinking && ( - - + = ({ isThinking, thinkingTim {messages.map((message, index) => { if (index < messages.length - 5) return null - return {message} + return ( +
+ {message} +
+ ) })} -
-
+ +
)} -
- - - - +
+ +
+ +
+ ) } -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 diff --git a/packages/ui/stories/components/display/Ellipsis.stories.tsx b/packages/ui/stories/components/display/Ellipsis.stories.tsx new file mode 100644 index 0000000000..38608e5ca3 --- /dev/null +++ b/packages/ui/stories/components/display/Ellipsis.stories.tsx @@ -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 + +export default meta +type Story = StoryObj + +// 默认单行省略 +export const Default: Story = { + args: { + maxLine: 1 + }, + render: (args) => ( +
+ +
+ ) +} + +// 多行省略 +export const MultiLine: Story = { + args: { + maxLine: 3, + children: + '这是一段很长的文本内容,用于演示多行省略功能的效果。当文本内容超过指定的最大行数时,会在最后一行的末尾显示省略号。这个功能特别适用于显示文章摘要、商品描述等需要限制显示行数的场景。' + }, + render: (args) => ( +
+ +
+ ) +} + +// 不同的最大行数 +export const DifferentMaxLines: Story = { + render: () => ( +
+
+

单行省略 (maxLine = 1)

+
+ 这是一段很长的文本内容,用于演示单行省略功能的效果。 +
+
+ +
+

两行省略 (maxLine = 2)

+
+ + 这是一段很长的文本内容,用于演示两行省略功能的效果。当文本内容超过两行时,会在第二行的末尾显示省略号。 + +
+
+ +
+

三行省略 (maxLine = 3)

+
+ + 这是一段很长的文本内容,用于演示三行省略功能的效果。当文本内容超过三行时,会在第三行的末尾显示省略号。这个功能特别适用于显示文章摘要、商品描述等需要限制显示行数的场景。 + +
+
+
+ ) +} + +// 短文本(不需要省略) +export const ShortText: Story = { + args: { + maxLine: 2, + children: '这是一段短文本。' + }, + render: (args) => ( +
+ +
+ ) +} + +// 自定义样式 +export const CustomStyle: Story = { + args: { + maxLine: 2, + className: 'text-blue-600 font-medium text-lg', + children: '这是一段带有自定义样式的长文本内容,用于演示如何自定义省略文本的样式。' + }, + render: (args) => ( +
+ +
+ ) +} + +// 不同容器宽度的响应式展示 +export const ResponsiveWidth: Story = { + render: () => ( +
+
+

窄容器 (200px)

+
+ 这是一段在窄容器中显示的文本内容,用于演示在不同宽度下的省略效果。 +
+
+ +
+

中等容器 (300px)

+
+ 这是一段在中等宽度容器中显示的文本内容,用于演示在不同宽度下的省略效果。 +
+
+ +
+

宽容器 (400px)

+
+ 这是一段在宽容器中显示的文本内容,用于演示在不同宽度下的省略效果。 +
+
+
+ ) +} + +// 包含HTML内容 +export const WithHTMLContent: Story = { + args: { + maxLine: 2 + }, + render: (args) => ( +
+ + 这是红色文本加粗文本 + 以及 + 斜体文本 + 组合的长文本内容,用于演示包含HTML元素的省略效果。 + +
+ ) +} diff --git a/packages/ui/stories/components/display/ListItem.stories.tsx b/packages/ui/stories/components/display/ListItem.stories.tsx new file mode 100644 index 0000000000..c700e72170 --- /dev/null +++ b/packages/ui/stories/components/display/ListItem.stories.tsx @@ -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 = { + 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 + +export default meta +type Story = StoryObj + +// 默认样式 +export const Default: Story = { + args: { + title: '默认列表项' + }, + render: (args) => ( +
+ +
+ ) +} + +// 带图标 +export const WithIcon: Story = { + args: { + icon: , + title: '带图标的列表项', + subtitle: '这是一个副标题' + }, + render: (args) => ( +
+ +
+ ) +} + +// 激活状态 +export const Active: Story = { + args: { + icon: , + title: '激活状态的列表项', + subtitle: '当前选中项', + active: true + }, + render: (args) => ( +
+ +
+ ) +} + +// 带右侧内容 +export const WithRightContent: Story = { + args: { + icon: , + title: '带右侧内容的列表项', + subtitle: '右侧有附加信息', + rightContent: + }, + render: (args) => ( +
+ +
+ ) +} + +// 多种图标类型 +export const DifferentIcons: Story = { + render: () => ( +
+ } + title="文件项" + subtitle="文档文件" + onClick={action('file-clicked')} + /> + } + title="文件夹项" + subtitle="目录文件夹" + onClick={action('folder-clicked')} + /> + } + title="用户项" + subtitle="用户信息" + onClick={action('user-clicked')} + /> + } + title="设置项" + subtitle="系统设置" + onClick={action('settings-clicked')} + /> +
+ ) +} + +// 不同长度的标题和副标题 +export const DifferentContentLength: Story = { + render: () => ( +
+ } title="短标题" subtitle="短副标题" /> + } + title="这是一个比较长的标题,可能会被截断" + subtitle="这也是一个比较长的副标题,用于测试文本溢出效果" + /> + } + title="超级长的标题内容用于测试文本省略功能,当标题过长时会自动截断并显示省略号" + subtitle="超级长的副标题内容用于测试文本省略功能,当副标题过长时也会自动截断" + /> +
+ ) +} + +// 不同的右侧内容类型 +export const DifferentRightContent: Story = { + render: () => ( +
+ } + title="带箭头" + subtitle="导航类型" + rightContent={} + /> + } + title="带按钮" + subtitle="操作类型" + rightContent={ + + } + /> + } + title="带文本" + subtitle="信息显示" + rightContent={在线} + /> + } + title="带多个操作" + subtitle="复合操作" + rightContent={ +
+ + +
+ } + /> +
+ ) +} + +// 激活状态对比 +export const ActiveComparison: Story = { + render: () => ( +
+

普通状态

+ } + title="普通列表项" + subtitle="未激活状态" + rightContent={} + /> + +

激活状态

+ } + title="激活列表项" + subtitle="当前选中状态" + active={true} + rightContent={} + /> +
+ ) +} + +// 自定义标题样式 +export const CustomTitleStyle: Story = { + render: () => ( +
+ } + title="红色标题" + subtitle="自定义颜色" + titleStyle={{ color: '#ef4444', fontWeight: 'bold' }} + /> + } + title="大号标题" + subtitle="自定义大小" + titleStyle={{ fontSize: '16px', fontWeight: '600' }} + /> + } + title="斜体标题" + subtitle="自定义样式" + titleStyle={{ fontStyle: 'italic', color: '#6366f1' }} + /> +
+ ) +} + +// 无副标题 +export const WithoutSubtitle: Story = { + render: () => ( +
+ } title="只有标题的列表项" /> + } + title="另一个只有标题的项" + rightContent={} + /> +
+ ) +} + +// 无图标 +export const WithoutIcon: Story = { + render: () => ( +
+ + 标签} + /> +
+ ) +} + +// 完整功能展示 +export const FullFeatures: Story = { + render: () => ( +
+ } + title="完整功能展示" + subtitle="包含所有功能的列表项" + titleStyle={{ fontWeight: '600' }} + active={true} + rightContent={ +
+ NEW + +
+ } + onClick={action('full-features-clicked')} + className="hover:shadow-sm transition-shadow" + /> +
+ ) +} diff --git a/packages/ui/stories/components/display/MaxContextCount.stories.tsx b/packages/ui/stories/components/display/MaxContextCount.stories.tsx new file mode 100644 index 0000000000..361ce1cb6d --- /dev/null +++ b/packages/ui/stories/components/display/MaxContextCount.stories.tsx @@ -0,0 +1,299 @@ +import type { Meta, StoryObj } from '@storybook/react' + +import MaxContextCount from '../../../src/components/display/MaxContextCount' + +const meta: Meta = { + 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 + +export default meta +type Story = StoryObj + +// 默认数字显示 +export const Default: Story = { + args: { + maxContext: 10 + }, + render: (args) => ( +
+ 最大上下文: + +
+ ) +} + +// 无限符号显示 +export const InfinitySymbol: Story = { + args: { + maxContext: 100 + }, + render: (args) => ( +
+ 最大上下文: + +
+ ) +} + +// 不同的数值范围 +export const DifferentValues: Story = { + render: () => ( +
+
+
+
小数值
+
+ 上下文: + +
+
+ +
+
中等数值
+
+ 上下文: + +
+
+ +
+
大数值
+
+ 上下文: + +
+
+ +
+
无限
+
+ 上下文: + +
+
+
+
+ ) +} + +// 不同大小 +export const DifferentSizes: Story = { + render: () => ( +
+
+
+ 小号 (12px): + +
+
+ +
+
+ 默认 (14px): + +
+
+ +
+
+ 中号 (18px): + +
+
+ +
+
+ 大号 (24px): + +
+
+
+ ) +} + +// 无限符号不同大小对比 +export const InfinityDifferentSizes: Story = { + render: () => ( +
+

无限符号不同大小对比

+
+
+ 12px: + +
+
+ 16px: + +
+
+ 20px: + +
+
+ 28px: + +
+
+
+ ) +} + +// 自定义样式 +export const CustomStyles: Story = { + render: () => ( +
+
+ 红色数字: + +
+ +
+ 蓝色无限符号: + +
+ +
+ 带背景: + +
+ +
+ 带边框: + +
+
+ ) +} + +// 在实际使用场景中的展示 +export const InRealScenarios: Story = { + render: () => ( +
+
+
+ AI 模型配置 +
+
+
+ 模型: + GPT-4 +
+
+ 温度: + 0.7 +
+
+ 最大上下文: + +
+
+
+ +
+
+ 对话设置 +
+
+
+ 记忆长度: + +
+
+ 历史消息: + +
+
+
+
+ ) +} + +// 边界值测试 +export const EdgeCases: Story = { + render: () => ( +
+

边界值测试

+
+
+
零值
+ +
+ +
+
临界值 99
+ +
+ +
+
临界值 100
+ +
+
+
+ ) +} + +// 深色主题下的表现 +export const DarkTheme: Story = { + parameters: { + backgrounds: { default: 'dark' } + }, + render: () => ( +
+

深色主题下的表现

+
+
+ 普通数字: + +
+
+ 无限符号: + +
+
+ 自定义颜色: + +
+
+
+ ) +} diff --git a/packages/ui/stories/components/display/ThinkingEffect.stories.tsx b/packages/ui/stories/components/display/ThinkingEffect.stories.tsx new file mode 100644 index 0000000000..8faed695b8 --- /dev/null +++ b/packages/ui/stories/components/display/ThinkingEffect.stories.tsx @@ -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 = { + 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 + +export default meta +type Story = StoryObj + +// 默认思考状态 +export const Default: Story = { + args: { + isThinking: true, + thinkingTimeText: '思考中 2s', + content: `正在分析用户的问题\n查找相关信息\n整理回答思路`, + expanded: false + }, + render: (args) => ( +
+ +
+ ) +} + +// 非思考状态(静止) +export const NotThinking: Story = { + args: { + isThinking: false, + thinkingTimeText: '思考完成', + content: `已完成思考\n找到最佳答案\n准备响应`, + expanded: false + }, + render: (args) => ( +
+ +
+ ) +} + +// 展开状态 +export const Expanded: Story = { + args: { + isThinking: false, + thinkingTimeText: '思考用时 5s', + content: `第一步:理解问题本质\n第二步:分析可能的解决方案\n第三步:评估各方案的优缺点\n第四步:选择最优方案\n第五步:构建详细回答`, + expanded: true + }, + render: (args) => ( +
+ +
+ ) +} + +// 交互式演示 +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 ( +
+
+ + + +
+ + +
+ ) + } +} + +// 不同内容长度 +export const DifferentContentLength: Story = { + render: () => ( +
+
+

短内容

+ +
+ +
+

中等长度内容

+ +
+ +
+

长内容

+ +
+
+ ) +} + +// 不同的思考时间文本 +export const DifferentThinkingTime: Story = { + render: () => ( +
+ + + + + + + + + 思考完成 +
+ } + content={`成功找到解决方案\n可以开始回答`} + expanded={false} + /> + + ) +} + +// 空内容状态 +export const EmptyContent: Story = { + render: () => ( +
+
+

无内容 - 思考中

+ +
+ +
+

无内容 - 停止思考

+ +
+
+ ) +} + +// 实时内容更新演示 +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 ( +
+
+ + +
+ + +
+ ) + } +} + +// 自定义样式 +export const CustomStyles: Story = { + render: () => ( +
+
+

自定义边框和背景

+ +
+ +
+

圆角和阴影

+ +
+
+ ) +} + +// 错误和边界情况 +export const EdgeCases: Story = { + render: () => ( +
+
+

单行内容

+ +
+ +
+

超长单行

+ +
+ +
+

特殊字符

+ +
+
+ ) +} diff --git a/packages/ui/tsconfig.json b/packages/ui/tsconfig.json index 64fd3505e8..b41decfb9e 100644 --- a/packages/ui/tsconfig.json +++ b/packages/ui/tsconfig.json @@ -13,7 +13,7 @@ "noFallthroughCasesInSwitch": true, "outDir": "./dist", "resolveJsonModule": true, - "rootDir": "./src", + "rootDir": ".", "skipLibCheck": true, "strict": true, "target": "ES2020" diff --git a/packages/ui/tsdown.config.ts b/packages/ui/tsdown.config.ts index e6f57dac94..5ec6a3dc93 100644 --- a/packages/ui/tsdown.config.ts +++ b/packages/ui/tsdown.config.ts @@ -12,9 +12,6 @@ export default defineConfig({ clean: true, dts: true, tsconfig: 'tsconfig.json', - alias: { - '@shared': '../shared' - }, // 将 HeroUI、Tailwind 和其他 peer dependencies 标记为外部依赖 external: [ 'react',