feat(ui): integrate @storybook/addon-themes and enhance CustomCollapse component

- Added @storybook/addon-themes to package.json and yarn.lock for theme support in Storybook.
- Updated CustomCollapse component to utilize HeroUI's Accordion and AccordionItem for improved functionality and styling.
- Removed the ReasoningIcon component as it was deemed unnecessary.
- Enhanced ProviderAvatar component to ensure consistent className handling.
- Added new stories for FileIcons, SvgSpinners180Ring, and ToolsCallingIcon to showcase their usage and variations.
This commit is contained in:
MyPrototypeWhat 2025-09-17 14:47:26 +08:00
parent 355e5b269d
commit 76b3ba5d7e
16 changed files with 1426 additions and 240 deletions

View File

@ -2,7 +2,7 @@ import type { StorybookConfig } from '@storybook/react-vite'
const config: StorybookConfig = { const config: StorybookConfig = {
stories: ['../stories/components/**/*.stories.@(js|jsx|ts|tsx)'], stories: ['../stories/components/**/*.stories.@(js|jsx|ts|tsx)'],
addons: ['@storybook/addon-docs'], addons: ['@storybook/addon-docs', '@storybook/addon-themes'],
framework: '@storybook/react-vite', framework: '@storybook/react-vite',
viteFinal: async (config) => { viteFinal: async (config) => {
const { mergeConfig } = await import('vite') const { mergeConfig } = await import('vite')

View File

@ -1,16 +1,18 @@
import '../stories/tailwind.css' import '../stories/tailwind.css'
import { withThemeByClassName } from '@storybook/addon-themes'
import type { Preview } from '@storybook/react' import type { Preview } from '@storybook/react'
const preview: Preview = { const preview: Preview = {
parameters: { decorators: [
controls: { withThemeByClassName({
matchers: { themes: {
color: /(background|color)$/i, light: '',
date: /Date$/i dark: 'dark'
} },
} defaultTheme: 'light'
} })
]
} }
export default preview export default preview

View File

@ -49,6 +49,7 @@
"devDependencies": { "devDependencies": {
"@heroui/react": "^2.8.4", "@heroui/react": "^2.8.4",
"@storybook/addon-docs": "^9.1.6", "@storybook/addon-docs": "^9.1.6",
"@storybook/addon-themes": "^9.1.6",
"@storybook/react-vite": "^9.1.6", "@storybook/react-vite": "^9.1.6",
"@types/react": "^19.0.12", "@types/react": "^19.0.12",
"@types/react-dom": "^19.0.4", "@types/react-dom": "^19.0.4",

View File

@ -1,7 +1,9 @@
// Original path: src/renderer/src/components/CustomCollapse.tsx import { Accordion, AccordionItem } from '@heroui/react'
import { ChevronRight } from 'lucide-react'
import type { FC } from 'react' import type { FC } from 'react'
import { memo, useState } from 'react' import { memo, useEffect, useState } from 'react'
// 重新导出 HeroUI 的组件,方便直接使用
export { Accordion, AccordionItem } from '@heroui/react'
interface CustomCollapseProps { interface CustomCollapseProps {
label: React.ReactNode label: React.ReactNode
@ -12,59 +14,78 @@ interface CustomCollapseProps {
activeKey?: string[] activeKey?: string[]
collapsible?: 'header' | 'icon' | 'disabled' collapsible?: 'header' | 'icon' | 'disabled'
onChange?: (activeKeys: string | string[]) => void onChange?: (activeKeys: string | string[]) => void
style?: React.CSSProperties
classNames?: {
trigger?: string
content?: string
}
className?: string className?: string
variant?: 'light' | 'shadow' | 'bordered' | 'splitted'
} }
const CustomCollapse: FC<CustomCollapseProps> = ({ const CustomCollapse: FC<CustomCollapseProps> = ({
label, label,
extra, extra,
children, children,
destroyInactivePanel = false,
defaultActiveKey = ['1'], defaultActiveKey = ['1'],
activeKey, activeKey,
collapsible = undefined, collapsible,
onChange, onChange,
className = '' style,
classNames,
className = '',
variant = 'bordered'
}) => { }) => {
const [isOpen, setIsOpen] = useState(activeKey ? activeKey.includes('1') : defaultActiveKey.includes('1')) const [expandedKeys, setExpandedKeys] = useState<Set<string>>(() => {
if (activeKey !== undefined) {
return new Set(activeKey)
}
return new Set(defaultActiveKey)
})
const handleToggle = () => { useEffect(() => {
if (collapsible === 'disabled') return if (activeKey !== undefined) {
setExpandedKeys(new Set(activeKey))
}
}, [activeKey])
const newState = !isOpen const handleSelectionChange = (keys: 'all' | Set<React.Key>) => {
setIsOpen(newState) if (keys === 'all') return
onChange?.(newState ? ['1'] : [])
const stringKeys = Array.from(keys).map((key) => String(key))
const newExpandedKeys = new Set(stringKeys)
if (activeKey === undefined) {
setExpandedKeys(newExpandedKeys)
}
onChange?.(stringKeys.length === 1 ? stringKeys[0] : stringKeys)
} }
const shouldRenderContent = !destroyInactivePanel || isOpen const isDisabled = collapsible === 'disabled'
return ( return (
<div className={`w-full bg-transparent border border-gray-200 dark:border-gray-700 ${className}`}> <Accordion
<div className={className}
className={`flex items-center justify-between px-4 py-1 cursor-pointer bg-gray-50 dark:bg-gray-800 ${ style={style}
isOpen ? 'rounded-t-lg' : 'rounded-lg' variant={variant}
} ${collapsible === 'disabled' ? 'cursor-default' : ''}`} defaultExpandedKeys={activeKey === undefined ? defaultActiveKey : undefined}
onClick={collapsible === 'header' || collapsible === undefined ? handleToggle : undefined}> selectedKeys={activeKey !== undefined ? expandedKeys : undefined}
<div className="flex items-center justify-between w-full"> onSelectionChange={handleSelectionChange}
<div className="flex items-center"> isDisabled={isDisabled}
{(collapsible === 'icon' || collapsible === undefined) && ( selectionMode="multiple">
<div className="mr-2 cursor-pointer" onClick={collapsible === 'icon' ? handleToggle : undefined}> <AccordionItem
<ChevronRight key="1"
size={16} aria-label={typeof label === 'string' ? label : 'collapse-item'}
className={`text-gray-500 dark:text-gray-400 transition-transform duration-200 ${ title={label}
isOpen ? 'rotate-90' : 'rotate-0' startContent={extra}
}`} classNames={{
strokeWidth={1.5} trigger: classNames?.trigger ?? '',
/> content: classNames?.content ?? ''
</div> }}>
)} {children}
{label} </AccordionItem>
</div> </Accordion>
{extra && <div>{extra}</div>}
</div>
</div>
{isOpen && <div className="border-t-0">{shouldRenderContent && children}</div>}
</div>
) )
} }

View File

@ -19,7 +19,7 @@ export const ProviderAvatar: React.FC<ProviderAvatarProps> = ({
providerName, providerName,
logoSrc, logoSrc,
size = 'md', size = 'md',
className, className = '',
style, style,
renderCustomLogo renderCustomLogo
}) => { }) => {
@ -51,7 +51,7 @@ export const ProviderAvatar: React.FC<ProviderAvatarProps> = ({
if (customLogo) { if (customLogo) {
return ( return (
<div <div
className={`flex items-center justify-center rounded-full border border-gray-200 dark:border-gray-700 ${className || ''}`} className={`flex items-center justify-center rounded-full border border-gray-200 dark:border-gray-700 ${className}`}
style={getCustomStyle()}> style={getCustomStyle()}>
<div className="w-4/5 h-4/5 flex items-center justify-center">{customLogo}</div> <div className="w-4/5 h-4/5 flex items-center justify-center">{customLogo}</div>
</div> </div>
@ -65,7 +65,7 @@ export const ProviderAvatar: React.FC<ProviderAvatarProps> = ({
<Avatar <Avatar
src={logoSrc} src={logoSrc}
size={getAvatarSize()} size={getAvatarSize()}
className={`border border-gray-200 dark:border-gray-700 ${className || ''}`} className={`border border-gray-200 dark:border-gray-700 ${className}`}
style={getCustomStyle()} style={getCustomStyle()}
imgProps={{ draggable: false }} imgProps={{ draggable: false }}
/> />
@ -80,7 +80,7 @@ export const ProviderAvatar: React.FC<ProviderAvatarProps> = ({
<Avatar <Avatar
name={getFirstCharacter(providerName)} name={getFirstCharacter(providerName)}
size={getAvatarSize()} size={getAvatarSize()}
className={`border border-gray-200 dark:border-gray-700 ${className || ''}`} className={`border border-gray-200 dark:border-gray-700 ${className}`}
style={{ style={{
backgroundColor, backgroundColor,
color, color,

View File

@ -1,32 +0,0 @@
// Original path: src/renderer/src/components/Icons/ReasoningIcon.tsx
import { Tooltip } from 'antd'
import type { FC } from 'react'
import React from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
const ReasoningIcon: FC<React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement>> = (props) => {
const { t } = useTranslation()
return (
<Container>
<Tooltip title={t('models.type.reasoning')} placement="top">
<Icon className="iconfont icon-thinking" {...(props as any)} />
</Tooltip>
</Container>
)
}
const Container = styled.div`
display: flex;
justify-content: center;
align-items: center;
`
const Icon = styled.i`
color: var(--color-link);
font-size: 16px;
margin-right: 6px;
`
export default ReasoningIcon

View File

@ -1,8 +1,14 @@
// Original path: src/renderer/src/components/Icons/SvgSpinners180Ring.tsx // Original path: src/renderer/src/components/Icons/SvgSpinners180Ring.tsx
import type { SVGProps } from 'react' import type { SVGProps } from 'react'
export function SvgSpinners180Ring(props: SVGProps<SVGSVGElement> & { size?: number | string }) { import { cn } from '../../../utils'
const { size = '1em', ...svgProps } = props
interface SvgSpinners180RingProps extends SVGProps<SVGSVGElement> {
size?: number | string
}
export function SvgSpinners180Ring(props: SvgSpinners180RingProps) {
const { size = '1em', className, ...svgProps } = props
return ( return (
<svg <svg
@ -11,7 +17,7 @@ export function SvgSpinners180Ring(props: SVGProps<SVGSVGElement> & { size?: num
height={size} height={size}
viewBox="0 0 24 24" viewBox="0 0 24 24"
{...svgProps} {...svgProps}
className={`animation-rotate ${svgProps.className || ''}`.trim()}> className={cn('animate-spin', className)}>
{/* Icon from SVG Spinners by Utkarsh Verma - https://github.com/n3r4zzurr0/svg-spinners/blob/main/LICENSE */} {/* Icon from SVG Spinners by Utkarsh Verma - https://github.com/n3r4zzurr0/svg-spinners/blob/main/LICENSE */}
<path <path
fill="currentColor" fill="currentColor"
@ -19,4 +25,5 @@ export function SvgSpinners180Ring(props: SVGProps<SVGSVGElement> & { size?: num
</svg> </svg>
) )
} }
export default SvgSpinners180Ring export default SvgSpinners180Ring

View File

@ -1,33 +1,26 @@
// Original: src/renderer/src/components/Icons/ToolsCallingIcon.tsx // Original: src/renderer/src/components/Icons/ToolsCallingIcon.tsx
import { ToolOutlined } from '@ant-design/icons' import { Tooltip } from '@heroui/react'
import { Tooltip } from 'antd' import { Wrench } from 'lucide-react'
import type { FC } from 'react'
import React from 'react' import React from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
const ToolsCallingIcon: FC<React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement>> = (props) => { import { cn } from '../../../utils'
interface ToolsCallingIconProps extends React.HTMLAttributes<HTMLDivElement> {
className?: string
iconClassName?: string
}
const ToolsCallingIcon = ({ className, iconClassName, ...props }: ToolsCallingIconProps) => {
const { t } = useTranslation() const { t } = useTranslation()
return ( return (
<Container> <div className={cn('flex justify-center items-center', className)} {...props}>
<Tooltip title={t('models.function_calling')} placement="top"> <Tooltip content={t('models.function_calling')} placement="top">
<Icon {...(props as any)} /> <Wrench className={cn('w-4 h-4 mr-1.5 text-[#00b96b]', iconClassName)} />
</Tooltip> </Tooltip>
</Container> </div>
) )
} }
const Container = styled.div`
display: flex;
justify-content: center;
align-items: center;
`
const Icon = styled(ToolOutlined)`
color: var(--color-primary);
font-size: 15px;
margin-right: 6px;
`
export default ToolsCallingIcon export default ToolsCallingIcon

View File

@ -42,7 +42,6 @@ export {
WebSearchIcon, WebSearchIcon,
WrapIcon WrapIcon
} from './icons/Icon' } from './icons/Icon'
export { default as ReasoningIcon } from './icons/ReasoningIcon'
export { default as SvgSpinners180Ring } from './icons/SvgSpinners180Ring' export { default as SvgSpinners180Ring } from './icons/SvgSpinners180Ring'
export { default as ToolsCallingIcon } from './icons/ToolsCallingIcon' export { default as ToolsCallingIcon } from './icons/ToolsCallingIcon'

View File

@ -1,9 +1,9 @@
import { Button } from '@heroui/react' import { Button } from '@heroui/react'
import type { Meta, StoryObj } from '@storybook/react' import type { Meta, StoryObj } from '@storybook/react'
import { AlertTriangle, Info, Settings } from 'lucide-react' import { AlertTriangle, CreditCard, Info, Monitor, Settings, Shield } from 'lucide-react'
import { useState } from 'react' import { useState } from 'react'
import CustomCollapse from '../../../src/components/base/CustomCollapse' import CustomCollapse, { Accordion, AccordionItem } from '../../../src/components/base/CustomCollapse'
const meta: Meta<typeof CustomCollapse> = { const meta: Meta<typeof CustomCollapse> = {
title: 'Base/CustomCollapse', title: 'Base/CustomCollapse',
@ -14,20 +14,16 @@ const meta: Meta<typeof CustomCollapse> = {
tags: ['autodocs'], tags: ['autodocs'],
argTypes: { argTypes: {
label: { label: {
control: false, control: 'text',
description: '折叠面板标题内容' description: '面板标题'
}, },
extra: { extra: {
control: false, control: false,
description: '标题栏右侧的额外内容' description: '额外内容(副标题)'
}, },
children: { children: {
control: false, control: false,
description: '折叠面板的内容' description: '面板内容'
},
destroyInactivePanel: {
control: 'boolean',
description: '是否销毁非活动面板的内容'
}, },
defaultActiveKey: { defaultActiveKey: {
control: false, control: false,
@ -37,14 +33,35 @@ const meta: Meta<typeof CustomCollapse> = {
control: false, control: false,
description: '当前激活的面板键值(受控模式)' description: '当前激活的面板键值(受控模式)'
}, },
collapsible: {
control: 'select',
options: ['header', 'icon', 'disabled', undefined],
description: '折叠触发方式'
},
onChange: { onChange: {
control: false, control: false,
description: '面板状态变化回调' description: '面板状态变化回调'
},
collapsible: {
control: 'select',
options: ['header', 'icon', 'disabled'],
description: '折叠触发方式'
},
className: {
control: 'text',
description: '额外的 CSS 类名'
},
variant: {
control: 'select',
options: ['light', 'shadow', 'bordered', 'splitted'],
description: 'HeroUI 样式变体'
},
destroyInactivePanel: {
control: 'boolean',
description: '是否销毁非激活面板'
},
style: {
control: false,
description: '自定义样式'
},
styles: {
control: false,
description: '自定义头部和内容样式'
} }
} }
} }
@ -52,6 +69,7 @@ const meta: Meta<typeof CustomCollapse> = {
export default meta export default meta
type Story = StoryObj<typeof meta> type Story = StoryObj<typeof meta>
// 基础用法
export const Default: Story = { export const Default: Story = {
args: { args: {
label: '默认折叠面板', label: '默认折叠面板',
@ -64,28 +82,78 @@ export const Default: Story = {
} }
} }
export const WithExtra: Story = { // 带副标题
export const WithSubtitle: Story = {
args: { args: {
label: '带额外内容的面板', label: '带副标题的折叠面板',
extra: ( extra: <span className="text-sm text-gray-500"></span>,
<Button size="sm" variant="ghost"> defaultActiveKey: ['1'],
</Button>
),
children: ( children: (
<div className="p-4"> <div className="p-4">
<p></p> <p></p>
<p>/</p> <p> extra </p>
</div> </div>
) )
} }
} }
export const WithIcon: Story = { // HeroUI 样式变体
export const VariantLight: Story = {
args: {
label: 'Light 变体',
variant: 'light',
children: (
<div className="p-4">
<p> HeroUI Light </p>
</div>
)
}
}
export const VariantShadow: Story = {
args: {
label: 'Shadow 变体',
extra: '带阴影的面板样式',
variant: 'shadow',
className: 'p-2',
children: (
<div className="p-4">
<p> HeroUI Shadow </p>
</div>
)
}
}
export const VariantBordered: Story = {
args: {
label: 'Bordered 变体(默认)',
variant: 'bordered',
children: (
<div className="p-4">
<p> HeroUI Bordered </p>
</div>
)
}
}
export const VariantSplitted: Story = {
args: {
label: 'Splitted 变体',
variant: 'splitted',
children: (
<div className="p-4">
<p> HeroUI Splitted </p>
</div>
)
}
}
// 富内容标题
export const RichLabel: Story = {
args: { args: {
label: ( label: (
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Settings size={16} /> <Settings className="text-default-500" size={20} />
<span></span> <span></span>
</div> </div>
), ),
@ -110,71 +178,172 @@ export const WithIcon: Story = {
} }
} }
export const CollapsibleHeader: Story = { // 带警告提示
export const WithWarning: Story = {
args: { args: {
label: '点击整个标题栏展开/收起', label: (
collapsible: 'header', <div className="flex items-center gap-2">
<Monitor className="text-primary" size={20} />
<span></span>
</div>
),
extra: (
<p className="flex">
2<span className="text-primary ml-1"></span>
</p>
),
children: ( children: (
<div className="p-4"> <div className="p-4">
<p> collapsible="header"/</p> <p className="text-small"></p>
</div> <ul className="list-disc list-inside mt-2 text-small space-y-1">
) <li></li>
} <li></li>
} </ul>
export const CollapsibleIcon: Story = {
args: {
label: '仅点击图标展开/收起',
collapsible: 'icon',
children: (
<div className="p-4">
<p> collapsible="icon"/</p>
</div> </div>
) )
} }
} }
// 禁用状态
export const Disabled: Story = { export const Disabled: Story = {
args: { args: {
label: '禁用的折叠面板', label: '禁用的折叠面板',
collapsible: 'disabled', collapsible: 'disabled',
defaultActiveKey: ['1'],
children: ( children: (
<div className="p-4"> <div className="p-4">
<p></p> <p></p>
</div> </div>
) )
} }
} }
export const DestroyInactivePanel: Story = { // 受控模式
args: { export const ControlledMode: Story = {
label: '销毁非活动内容', render: function ControlledMode() {
destroyInactivePanel: true, const [activeKey, setActiveKey] = useState<string[]>(['1'])
children: (
<div className="p-4"> return (
<p> destroyInactivePanel=true </p> <div className="space-y-4">
<p>{new Date().toLocaleTimeString()}</p> <div className="flex gap-2">
<Button size="sm" onPress={() => setActiveKey(['1'])} color="primary">
</Button>
<Button size="sm" onPress={() => setActiveKey([])} color="default">
</Button>
</div>
<CustomCollapse
label="受控的折叠面板"
activeKey={activeKey}
onChange={(keys) => setActiveKey(Array.isArray(keys) ? keys : [keys])}>
<div className="p-4">
<p></p>
<p></p>
</div>
</CustomCollapse>
<div className="text-sm text-gray-600">{activeKey.length > 0 ? '展开' : '收起'}</div>
</div> </div>
) )
} }
} }
// 多个单面板组合
export const MultipleSinglePanels: Story = {
render: () => (
<div className="space-y-4">
<CustomCollapse label="第一个面板" defaultActiveKey={['1']}>
<div className="p-4">
<p></p>
</div>
</CustomCollapse>
<CustomCollapse label="第二个面板" extra="带副标题">
<div className="p-4">
<p></p>
</div>
</CustomCollapse>
<CustomCollapse label="第三个面板(禁用)" collapsible="disabled">
<div className="p-4">
<p></p>
</div>
</CustomCollapse>
</div>
)
}
// 使用原生 HeroUI Accordion 的多面板示例
export const NativeAccordionMultiple: Story = {
render: () => (
<div className="max-w-lg">
<h3 className="text-lg font-medium mb-4"> HeroUI Accordion </h3>
<Accordion variant="shadow" className="p-2 flex flex-col gap-1" defaultExpandedKeys={['1']}>
<AccordionItem
key="1"
title="连接的设备"
startContent={<Monitor className="text-primary" size={20} />}
subtitle={
<p className="flex">
2<span className="text-primary ml-1"></span>
</p>
}>
<div className="p-4">
<p className="text-small"></p>
</div>
</AccordionItem>
<AccordionItem
key="2"
title="应用权限"
startContent={<Shield className="text-default-500" size={20} />}
subtitle="3个应用有读取权限">
<div className="p-4">
<p className="text-small"></p>
</div>
</AccordionItem>
<AccordionItem
key="3"
title="待办任务"
startContent={<Info className="text-warning" size={20} />}
subtitle={<span className="text-warning"></span>}>
<div className="p-4">
<p className="text-small"></p>
</div>
</AccordionItem>
<AccordionItem
key="4"
title={
<p className="flex gap-1 items-center">
<span className="text-default-400 text-small">*4812</span>
</p>
}
startContent={<CreditCard className="text-danger" size={20} />}
subtitle={<span className="text-danger"></span>}>
<div className="p-4">
<p className="text-small text-danger"></p>
</div>
</AccordionItem>
</Accordion>
</div>
)
}
// 富内容面板
export const RichContent: Story = { export const RichContent: Story = {
args: { args: {
label: ( label: (
<div className="flex items-center gap-2"> <div className="flex items-center justify-between w-full">
<Info size={16} /> <div className="flex items-center gap-2">
<span></span> <Info className="text-default-500" size={20} />
</div> <span></span>
), </div>
extra: ( <div className="flex gap-2" onClick={(e) => e.stopPropagation()}>
<div className="flex gap-2"> <Button size="sm" variant="flat" color="primary">
<Button size="sm" variant="flat" color="primary">
</Button>
</Button> <Button size="sm" variant="flat">
<Button size="sm" variant="flat">
</Button>
</Button> </div>
</div> </div>
), ),
children: ( children: (
@ -202,82 +371,82 @@ export const RichContent: Story = {
</div> </div>
<div> <div>
<label className="block text-sm font-medium mb-1"></label> <label className="block text-sm font-medium mb-1"></label>
<textarea className="w-full px-3 py-2 border border-gray-300 rounded-md" rows={3} placeholder="请输入描述" /> <textarea
className="w-full px-3 py-2 border border-gray-300 rounded-md"
rows={3}
placeholder="请输入描述"
/>
</div> </div>
</div> </div>
) )
} }
} }
export const MultipleCollapse: Story = { // 自定义样式
render: () => ( export const CustomStyles: Story = {
<div className="space-y-4"> args: {
<h3 className="text-lg font-medium"></h3> label: (
<div className="space-y-2"> <div className="flex items-center gap-2">
<CustomCollapse <AlertTriangle className="text-warning" size={16} />
label="面板 1" <span></span>
defaultActiveKey={['1']}
children={
<div className="p-4">
<p></p>
</div>
}
/>
<CustomCollapse
label={
<div className="flex items-center gap-2">
<AlertTriangle size={16} className="text-yellow-500" />
<span></span>
</div>
}
defaultActiveKey={[]}
children={
<div className="p-4 bg-yellow-50 dark:bg-yellow-900/20">
<p className="text-yellow-800 dark:text-yellow-200"></p>
</div>
}
/>
<CustomCollapse
label="面板 3"
collapsible="icon"
extra={<span className="text-sm text-gray-500"></span>}
defaultActiveKey={[]}
children={
<div className="p-4">
<p>/</p>
</div>
}
/>
</div> </div>
</div> ),
) style: {
backgroundColor: 'rgba(255, 193, 7, 0.1)',
borderColor: 'var(--color-warning)'
},
children: (
<div className="p-4 bg-warning-50 dark:bg-warning-900/20">
<p className="text-warning-800 dark:text-warning-200"></p>
</div>
)
}
} }
export const ControlledMode: Story = { // 原生 HeroUI Accordion 多面板受控模式
render: function ControlledMode() { export const NativeAccordionControlled: Story = {
const [activeKey, setActiveKey] = useState<string[]>(['1']) render: function NativeAccordionControlled() {
const [activeKeys, setActiveKeys] = useState<Set<string>>(new Set(['1']))
return ( return (
<div className="space-y-4"> <div className="space-y-4">
<div className="flex gap-2"> <div className="flex gap-2">
<Button size="sm" onPress={() => setActiveKey(['1'])} color={activeKey.includes('1') ? 'primary' : 'default'}> <Button size="sm" onPress={() => setActiveKeys(new Set(['1', '2', '3']))} color="primary">
</Button> </Button>
<Button size="sm" onPress={() => setActiveKey([])} color={!activeKey.includes('1') ? 'primary' : 'default'}> <Button size="sm" onPress={() => setActiveKeys(new Set())} color="default">
</Button>
<Button size="sm" onPress={() => setActiveKeys(new Set(['2']))} color="default">
</Button> </Button>
</div> </div>
<CustomCollapse <Accordion
label="受控模式" selectedKeys={activeKeys}
activeKey={activeKey} onSelectionChange={(keys) => {
onChange={(keys) => setActiveKey(Array.isArray(keys) ? keys : [keys])} if (keys !== 'all') {
children={ setActiveKeys(keys)
}
}}>
<AccordionItem key="1" title="受控面板 1">
<div className="p-4"> <div className="p-4">
<p>/</p> <p></p>
<p>{activeKey.includes('1') ? '展开' : '收起'}</p>
</div> </div>
} </AccordionItem>
/> <AccordionItem key="2" title="受控面板 2">
<div className="p-4">
<p></p>
</div>
</AccordionItem>
<AccordionItem key="3" title="受控面板 3">
<div className="p-4">
<p></p>
</div>
</AccordionItem>
</Accordion>
<div className="text-sm text-gray-600">
{activeKeys.size > 0 ? Array.from(activeKeys).join(', ') : '无'}
</div>
</div> </div>
) )
} }

View File

@ -0,0 +1,270 @@
import type { Meta, StoryObj } from '@storybook/react'
import { FilePngIcon, FileSvgIcon } from '../../../src/components/icons/FileIcons'
// Create a dummy component for the story
const FileIconsShowcase = () => <div />
const meta: Meta<typeof FileIconsShowcase> = {
title: 'Icons/FileIcons',
component: FileIconsShowcase,
parameters: {
layout: 'centered'
},
tags: ['autodocs'],
argTypes: {
size: {
description: '图标大小',
control: { type: 'text' },
defaultValue: '1.1em'
}
}
}
export default meta
type Story = StoryObj<typeof meta>
// Basic File Icons
export const BasicFileIcons: Story = {
render: () => (
<div className="space-y-6">
<div>
<h3 className="mb-3 font-semibold"> (默认尺寸: 1.1em)</h3>
<div className="flex items-center gap-6">
<div className="flex flex-col items-center gap-2">
<FileSvgIcon />
<span className="text-xs text-gray-600">SVG </span>
</div>
<div className="flex flex-col items-center gap-2">
<FilePngIcon />
<span className="text-xs text-gray-600">PNG </span>
</div>
</div>
</div>
</div>
)
}
// Different Sizes
export const DifferentSizes: Story = {
render: () => (
<div className="space-y-6">
<div>
<h3 className="mb-3 font-semibold"> SVG </h3>
<div className="flex items-end gap-4">
<div className="flex flex-col items-center gap-2">
<FileSvgIcon size="16" />
<span className="text-xs text-gray-600">16px</span>
</div>
<div className="flex flex-col items-center gap-2">
<FileSvgIcon size="24" />
<span className="text-xs text-gray-600">24px</span>
</div>
<div className="flex flex-col items-center gap-2">
<FileSvgIcon size="32" />
<span className="text-xs text-gray-600">32px</span>
</div>
<div className="flex flex-col items-center gap-2">
<FileSvgIcon size="48" />
<span className="text-xs text-gray-600">48px</span>
</div>
<div className="flex flex-col items-center gap-2">
<FileSvgIcon size="64" />
<span className="text-xs text-gray-600">64px</span>
</div>
</div>
</div>
<div>
<h3 className="mb-3 font-semibold"> PNG </h3>
<div className="flex items-end gap-4">
<div className="flex flex-col items-center gap-2">
<FilePngIcon size="16" />
<span className="text-xs text-gray-600">16px</span>
</div>
<div className="flex flex-col items-center gap-2">
<FilePngIcon size="24" />
<span className="text-xs text-gray-600">24px</span>
</div>
<div className="flex flex-col items-center gap-2">
<FilePngIcon size="32" />
<span className="text-xs text-gray-600">32px</span>
</div>
<div className="flex flex-col items-center gap-2">
<FilePngIcon size="48" />
<span className="text-xs text-gray-600">48px</span>
</div>
<div className="flex flex-col items-center gap-2">
<FilePngIcon size="64" />
<span className="text-xs text-gray-600">64px</span>
</div>
</div>
</div>
</div>
)
}
// Custom Colors
export const CustomColors: Story = {
render: () => (
<div className="space-y-6">
<div>
<h3 className="mb-3 font-semibold"> - SVG </h3>
<div className="flex items-center gap-4">
<div className="flex flex-col items-center gap-2">
<FileSvgIcon size="32" color="#3B82F6" />
<span className="text-xs text-gray-600"></span>
</div>
<div className="flex flex-col items-center gap-2">
<FileSvgIcon size="32" color="#10B981" />
<span className="text-xs text-gray-600">绿</span>
</div>
<div className="flex flex-col items-center gap-2">
<FileSvgIcon size="32" color="#F59E0B" />
<span className="text-xs text-gray-600"></span>
</div>
<div className="flex flex-col items-center gap-2">
<FileSvgIcon size="32" color="#EF4444" />
<span className="text-xs text-gray-600"></span>
</div>
<div className="flex flex-col items-center gap-2">
<FileSvgIcon size="32" color="#8B5CF6" />
<span className="text-xs text-gray-600"></span>
</div>
</div>
</div>
<div>
<h3 className="mb-3 font-semibold"> - PNG </h3>
<div className="flex items-center gap-4">
<div className="flex flex-col items-center gap-2">
<FilePngIcon size="32" color="#3B82F6" />
<span className="text-xs text-gray-600"></span>
</div>
<div className="flex flex-col items-center gap-2">
<FilePngIcon size="32" color="#10B981" />
<span className="text-xs text-gray-600">绿</span>
</div>
<div className="flex flex-col items-center gap-2">
<FilePngIcon size="32" color="#F59E0B" />
<span className="text-xs text-gray-600"></span>
</div>
<div className="flex flex-col items-center gap-2">
<FilePngIcon size="32" color="#EF4444" />
<span className="text-xs text-gray-600"></span>
</div>
<div className="flex flex-col items-center gap-2">
<FilePngIcon size="32" color="#8B5CF6" />
<span className="text-xs text-gray-600"></span>
</div>
</div>
</div>
</div>
)
}
// In File List Context
export const InFileListContext: Story = {
render: () => (
<div className="space-y-4">
<h3 className="mb-3 font-semibold">使</h3>
<div className="rounded-lg border border-gray-200 p-4">
<div className="space-y-3">
<div className="flex items-center gap-3 rounded p-2 hover:bg-gray-50">
<FileSvgIcon size="20" />
<span className="flex-1">illustration.svg</span>
<span className="text-xs text-gray-500">45 KB</span>
</div>
<div className="flex items-center gap-3 rounded p-2 hover:bg-gray-50">
<FilePngIcon size="20" />
<span className="flex-1">screenshot.png</span>
<span className="text-xs text-gray-500">1.2 MB</span>
</div>
<div className="flex items-center gap-3 rounded p-2 hover:bg-gray-50">
<FileSvgIcon size="20" />
<span className="flex-1">logo.svg</span>
<span className="text-xs text-gray-500">12 KB</span>
</div>
<div className="flex items-center gap-3 rounded p-2 hover:bg-gray-50">
<FilePngIcon size="20" />
<span className="flex-1">background.png</span>
<span className="text-xs text-gray-500">2.8 MB</span>
</div>
</div>
</div>
</div>
)
}
// File Type Grid
export const FileTypeGrid: Story = {
render: () => (
<div className="space-y-4">
<h3 className="mb-3 font-semibold"></h3>
<div className="grid grid-cols-4 gap-4">
<div className="flex flex-col items-center gap-2 rounded-lg border border-gray-200 p-4 hover:border-blue-500">
<FileSvgIcon size="48" />
<span className="text-sm font-medium">SVG</span>
<span className="text-xs text-gray-600"></span>
</div>
<div className="flex flex-col items-center gap-2 rounded-lg border border-gray-200 p-4 hover:border-blue-500">
<FilePngIcon size="48" />
<span className="text-sm font-medium">PNG</span>
<span className="text-xs text-gray-600"></span>
</div>
<div className="flex flex-col items-center gap-2 rounded-lg border border-gray-200 p-4 hover:border-blue-500">
<FileSvgIcon size="48" color="#10B981" />
<span className="text-sm font-medium">SVG</span>
<span className="text-xs text-gray-600"></span>
</div>
<div className="flex flex-col items-center gap-2 rounded-lg border border-gray-200 p-4 hover:border-blue-500">
<FilePngIcon size="48" color="#EF4444" />
<span className="text-sm font-medium">PNG</span>
<span className="text-xs text-gray-600"></span>
</div>
</div>
</div>
)
}
// Interactive Example
export const InteractiveExample: Story = {
render: () => {
const fileTypes = [
{ icon: FileSvgIcon, name: 'Vector Graphics', ext: 'SVG', color: '#3B82F6' },
{ icon: FilePngIcon, name: 'Raster Image', ext: 'PNG', color: '#10B981' }
]
return (
<div className="space-y-4">
<h3 className="mb-3 font-semibold"></h3>
<div className="grid grid-cols-2 gap-4">
{fileTypes.map((fileType, index) => {
const IconComponent = fileType.icon
return (
<button
key={index}
type="button"
className="flex items-center gap-3 rounded-lg border border-gray-200 p-4 text-left transition-all hover:border-blue-500 hover:shadow-md focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500/20">
<IconComponent size="32" color={fileType.color} />
<div>
<div className="font-medium">{fileType.ext} </div>
<div className="text-sm text-gray-600">{fileType.name}</div>
</div>
</button>
)
})}
</div>
</div>
)
}
}

View File

@ -25,7 +25,22 @@ const meta: Meta<typeof IconShowcase> = {
parameters: { parameters: {
layout: 'centered' layout: 'centered'
}, },
tags: ['autodocs'] tags: ['autodocs'],
argTypes: {
size: {
description: '图标大小 (支持数字或字符串)',
control: { type: 'text' },
defaultValue: '1rem'
},
color: {
description: '图标颜色',
control: { type: 'color' }
},
className: {
description: '自定义 CSS 类名',
control: { type: 'text' }
}
}
} }
export default meta export default meta
@ -36,7 +51,7 @@ export const PredefinedIcons: Story = {
render: () => ( render: () => (
<div className="space-y-6"> <div className="space-y-6">
<div> <div>
<h3 className="mb-3 font-semibold">Predefined Icons (Default Size: 1rem)</h3> <h3 className="mb-3 font-semibold"> (默认尺寸: 1rem)</h3>
<div className="grid grid-cols-6 gap-4"> <div className="grid grid-cols-6 gap-4">
<div className="flex flex-col items-center gap-2"> <div className="flex flex-col items-center gap-2">
<CopyIcon /> <CopyIcon />
@ -138,7 +153,7 @@ export const CustomIconCreation: Story = {
return ( return (
<div className="space-y-6"> <div className="space-y-6">
<div> <div>
<h3 className="mb-3 font-semibold">Custom Icons Created with Factory</h3> <h3 className="mb-3 font-semibold">使</h3>
<div className="flex items-center gap-6"> <div className="flex items-center gap-6">
<div className="flex flex-col items-center gap-2"> <div className="flex flex-col items-center gap-2">
<SettingsIcon /> <SettingsIcon />
@ -160,7 +175,7 @@ export const CustomIconCreation: Story = {
</div> </div>
<div> <div>
<h3 className="mb-3 font-semibold">Override Default Size</h3> <h3 className="mb-3 font-semibold"></h3>
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
<SettingsIcon size={32} /> <SettingsIcon size={32} />
<DownloadIcon size={32} /> <DownloadIcon size={32} />

View File

@ -0,0 +1,340 @@
import type { Meta, StoryObj } from '@storybook/react'
import SvgSpinners180Ring from '../../../src/components/icons/SvgSpinners180Ring'
const meta: Meta<typeof SvgSpinners180Ring> = {
title: 'Icons/SvgSpinners180Ring',
component: SvgSpinners180Ring,
parameters: {
layout: 'centered'
},
tags: ['autodocs'],
argTypes: {
size: {
description: '加载图标大小',
control: { type: 'text' },
defaultValue: '1em'
},
className: {
description: '自定义 CSS 类名',
control: { type: 'text' }
}
}
}
export default meta
type Story = StoryObj<typeof meta>
// Basic Spinner
export const BasicSpinner: Story = {
render: () => (
<div className="space-y-6">
<div>
<h3 className="mb-3 font-semibold"></h3>
<div className="flex items-center gap-4">
<SvgSpinners180Ring />
<span className="text-sm text-gray-600"> (1em)</span>
</div>
</div>
</div>
)
}
// Different Sizes
export const DifferentSizes: Story = {
render: () => (
<div className="space-y-6">
<div>
<h3 className="mb-3 font-semibold"></h3>
<div className="flex items-end gap-6">
<div className="flex flex-col items-center gap-2">
<SvgSpinners180Ring size="12" />
<span className="text-xs text-gray-600">12px</span>
</div>
<div className="flex flex-col items-center gap-2">
<SvgSpinners180Ring size="16" />
<span className="text-xs text-gray-600">16px</span>
</div>
<div className="flex flex-col items-center gap-2">
<SvgSpinners180Ring size="20" />
<span className="text-xs text-gray-600">20px</span>
</div>
<div className="flex flex-col items-center gap-2">
<SvgSpinners180Ring size="24" />
<span className="text-xs text-gray-600">24px</span>
</div>
<div className="flex flex-col items-center gap-2">
<SvgSpinners180Ring size="32" />
<span className="text-xs text-gray-600">32px</span>
</div>
<div className="flex flex-col items-center gap-2">
<SvgSpinners180Ring size="48" />
<span className="text-xs text-gray-600">48px</span>
</div>
</div>
</div>
</div>
)
}
// Different Colors
export const DifferentColors: Story = {
render: () => (
<div className="space-y-6">
<div>
<h3 className="mb-3 font-semibold"></h3>
<div className="flex items-center gap-6">
<div className="flex flex-col items-center gap-2">
<SvgSpinners180Ring size="24" className="text-blue-500" />
<span className="text-xs text-gray-600"></span>
</div>
<div className="flex flex-col items-center gap-2">
<SvgSpinners180Ring size="24" className="text-green-500" />
<span className="text-xs text-gray-600">绿</span>
</div>
<div className="flex flex-col items-center gap-2">
<SvgSpinners180Ring size="24" className="text-orange-500" />
<span className="text-xs text-gray-600"></span>
</div>
<div className="flex flex-col items-center gap-2">
<SvgSpinners180Ring size="24" className="text-red-500" />
<span className="text-xs text-gray-600"></span>
</div>
<div className="flex flex-col items-center gap-2">
<SvgSpinners180Ring size="24" className="text-purple-500" />
<span className="text-xs text-gray-600"></span>
</div>
<div className="flex flex-col items-center gap-2">
<SvgSpinners180Ring size="24" className="text-gray-500" />
<span className="text-xs text-gray-600"></span>
</div>
</div>
</div>
</div>
)
}
// Loading States in Buttons
export const LoadingStatesInButtons: Story = {
render: () => (
<div className="space-y-6">
<div>
<h3 className="mb-3 font-semibold"></h3>
<div className="flex flex-wrap items-center gap-4">
<button
type="button"
className="flex items-center gap-2 rounded bg-blue-500 px-4 py-2 text-white hover:bg-blue-600"
disabled
>
<SvgSpinners180Ring size="16" />
<span>...</span>
</button>
<button
type="button"
className="flex items-center gap-2 rounded bg-green-500 px-4 py-2 text-white hover:bg-green-600"
disabled
>
<SvgSpinners180Ring size="16" />
<span></span>
</button>
<button
type="button"
className="flex items-center gap-2 rounded bg-orange-500 px-4 py-2 text-white hover:bg-orange-600"
disabled
>
<SvgSpinners180Ring size="16" />
<span></span>
</button>
<button
type="button"
className="flex items-center gap-2 rounded border border-gray-300 bg-white px-4 py-2 text-gray-700 hover:bg-gray-50"
disabled
>
<SvgSpinners180Ring size="16" className="text-gray-500" />
<span></span>
</button>
</div>
</div>
</div>
)
}
// Loading Cards
export const LoadingCards: Story = {
render: () => (
<div className="space-y-6">
<div>
<h3 className="mb-3 font-semibold"></h3>
<div className="grid grid-cols-2 gap-4">
<div className="rounded-lg border border-gray-200 p-4">
<div className="flex items-center gap-3">
<SvgSpinners180Ring size="20" className="text-blue-500" />
<div>
<h4 className="font-medium">AI </h4>
<p className="text-sm text-gray-600">...</p>
</div>
</div>
</div>
<div className="rounded-lg border border-gray-200 p-4">
<div className="flex items-center gap-3">
<SvgSpinners180Ring size="20" className="text-green-500" />
<div>
<h4 className="font-medium"></h4>
<p className="text-sm text-gray-600">75% </p>
</div>
</div>
</div>
<div className="rounded-lg border border-gray-200 p-4">
<div className="flex items-center gap-3">
<SvgSpinners180Ring size="20" className="text-orange-500" />
<div>
<h4 className="font-medium"></h4>
<p className="text-sm text-gray-600">...</p>
</div>
</div>
</div>
<div className="rounded-lg border border-gray-200 p-4">
<div className="flex items-center gap-3">
<SvgSpinners180Ring size="20" className="text-purple-500" />
<div>
<h4 className="font-medium"></h4>
<p className="text-sm text-gray-600">2</p>
</div>
</div>
</div>
</div>
</div>
</div>
)
}
// Inline Loading States
export const InlineLoadingStates: Story = {
render: () => (
<div className="space-y-6">
<div>
<h3 className="mb-3 font-semibold"></h3>
<div className="space-y-4">
<div className="flex items-center gap-2">
<SvgSpinners180Ring size="14" className="text-blue-500" />
<span className="text-sm">...</span>
</div>
<div className="flex items-center gap-2">
<SvgSpinners180Ring size="14" className="text-green-500" />
<span className="text-sm">...</span>
</div>
<div className="flex items-center gap-2">
<SvgSpinners180Ring size="14" className="text-orange-500" />
<span className="text-sm">...</span>
</div>
<div className="rounded bg-blue-50 p-3">
<div className="flex items-center gap-2">
<SvgSpinners180Ring size="16" className="text-blue-600" />
<span className="text-sm text-blue-800">...</span>
</div>
</div>
</div>
</div>
</div>
)
}
// Loading States with Different Speeds
export const LoadingStatesWithDifferentSpeeds: Story = {
render: () => (
<div className="space-y-6">
<div>
<h3 className="mb-3 font-semibold"></h3>
<div className="flex items-center gap-6">
<div className="flex flex-col items-center gap-2">
<SvgSpinners180Ring size="24" className="animate-spin" style={{ animationDuration: '2s' }} />
<span className="text-xs text-gray-600"> (2s)</span>
</div>
<div className="flex flex-col items-center gap-2">
<SvgSpinners180Ring size="24" />
<span className="text-xs text-gray-600"></span>
</div>
<div className="flex flex-col items-center gap-2">
<SvgSpinners180Ring size="24" className="animate-spin" style={{ animationDuration: '0.5s' }} />
<span className="text-xs text-gray-600"> (0.5s)</span>
</div>
</div>
</div>
</div>
)
}
// Full Page Loading
export const FullPageLoading: Story = {
render: () => (
<div className="space-y-6">
<div>
<h3 className="mb-3 font-semibold"></h3>
<div className="relative h-64 w-full overflow-hidden rounded-lg border border-gray-200 bg-white">
<div className="absolute inset-0 flex flex-col items-center justify-center bg-white/80">
<SvgSpinners180Ring size="32" className="text-blue-500" />
<p className="mt-4 text-sm text-gray-600">...</p>
</div>
{/* 模拟页面内容 */}
<div className="p-6 opacity-30">
<div className="mb-4 h-6 w-1/3 rounded bg-gray-200"></div>
<div className="mb-2 h-4 w-full rounded bg-gray-200"></div>
<div className="mb-2 h-4 w-5/6 rounded bg-gray-200"></div>
<div className="mb-4 h-4 w-4/6 rounded bg-gray-200"></div>
<div className="mb-2 h-4 w-full rounded bg-gray-200"></div>
<div className="h-4 w-3/4 rounded bg-gray-200"></div>
</div>
</div>
</div>
</div>
)
}
// Interactive Loading Demo
export const InteractiveLoadingDemo: Story = {
render: () => {
const loadingStates = [
{ text: '发送消息', color: 'text-blue-500', bgColor: 'bg-blue-500' },
{ text: '上传文件', color: 'text-green-500', bgColor: 'bg-green-500' },
{ text: '生成内容', color: 'text-purple-500', bgColor: 'bg-purple-500' },
{ text: '搜索结果', color: 'text-orange-500', bgColor: 'bg-orange-500' }
]
return (
<div className="space-y-4">
<h3 className="mb-3 font-semibold"></h3>
<div className="grid grid-cols-2 gap-4">
{loadingStates.map((state, index) => (
<button
key={index}
type="button"
className={`flex items-center justify-center gap-2 rounded-lg ${state.bgColor} px-4 py-3 text-white transition-all hover:opacity-90 focus:outline-none focus:ring-2 focus:ring-offset-2`}
onClick={() => {
// 演示用途 - 在实际应用中这里会触发真实的加载状态
alert(`触发 ${state.text} 加载状态`)
}}
>
<SvgSpinners180Ring size="16" />
<span>{state.text}...</span>
</button>
))}
</div>
<p className="text-xs text-gray-500">
</p>
</div>
)
}
}

View File

@ -0,0 +1,389 @@
import type { Meta, StoryObj } from '@storybook/react'
import ToolsCallingIcon from '../../../src/components/icons/ToolsCallingIcon'
const meta: Meta<typeof ToolsCallingIcon> = {
title: 'Icons/ToolsCallingIcon',
component: ToolsCallingIcon,
parameters: {
layout: 'centered'
},
tags: ['autodocs'],
argTypes: {
className: {
description: '容器的自定义 CSS 类名',
control: { type: 'text' }
},
iconClassName: {
description: '图标的自定义 CSS 类名',
control: { type: 'text' }
}
}
}
export default meta
type Story = StoryObj<typeof meta>
// Basic Tools Calling Icon
export const BasicToolsCallingIcon: Story = {
render: () => (
<div className="space-y-6">
<div>
<h3 className="mb-3 font-semibold"></h3>
<div className="flex items-center gap-4">
<ToolsCallingIcon />
</div>
<p className="mt-2 text-sm text-gray-600">
"函数调用"
</p>
</div>
</div>
)
}
// Different Sizes
export const DifferentSizes: Story = {
render: () => (
<div className="space-y-6">
<div>
<h3 className="mb-3 font-semibold"></h3>
<div className="flex items-end gap-6">
<div className="flex flex-col items-center gap-2">
<ToolsCallingIcon iconClassName="w-3 h-3" />
<span className="text-xs text-gray-600"></span>
</div>
<div className="flex flex-col items-center gap-2">
<ToolsCallingIcon />
<span className="text-xs text-gray-600"></span>
</div>
<div className="flex flex-col items-center gap-2">
<ToolsCallingIcon iconClassName="w-5 h-5" />
<span className="text-xs text-gray-600"></span>
</div>
<div className="flex flex-col items-center gap-2">
<ToolsCallingIcon iconClassName="w-6 h-6" />
<span className="text-xs text-gray-600"></span>
</div>
<div className="flex flex-col items-center gap-2">
<ToolsCallingIcon iconClassName="w-8 h-8" />
<span className="text-xs text-gray-600"></span>
</div>
</div>
</div>
</div>
)
}
// Different Colors
export const DifferentColors: Story = {
render: () => (
<div className="space-y-6">
<div>
<h3 className="mb-3 font-semibold"></h3>
<div className="flex items-center gap-6">
<div className="flex flex-col items-center gap-2">
<ToolsCallingIcon />
<span className="text-xs text-gray-600">绿</span>
</div>
<div className="flex flex-col items-center gap-2">
<ToolsCallingIcon iconClassName="w-4 h-4 mr-1.5 text-blue-500" />
<span className="text-xs text-gray-600"></span>
</div>
<div className="flex flex-col items-center gap-2">
<ToolsCallingIcon iconClassName="w-4 h-4 mr-1.5 text-orange-500" />
<span className="text-xs text-gray-600"></span>
</div>
<div className="flex flex-col items-center gap-2">
<ToolsCallingIcon iconClassName="w-4 h-4 mr-1.5 text-red-500" />
<span className="text-xs text-gray-600"></span>
</div>
<div className="flex flex-col items-center gap-2">
<ToolsCallingIcon iconClassName="w-4 h-4 mr-1.5 text-purple-500" />
<span className="text-xs text-gray-600"></span>
</div>
<div className="flex flex-col items-center gap-2">
<ToolsCallingIcon iconClassName="w-4 h-4 mr-1.5 text-gray-500" />
<span className="text-xs text-gray-600"></span>
</div>
</div>
</div>
</div>
)
}
// Model Features Context
export const ModelFeaturesContext: Story = {
render: () => (
<div className="space-y-4">
<h3 className="mb-3 font-semibold">使</h3>
<div className="grid gap-4">
<div className="rounded-lg border border-gray-200 p-4">
<div className="mb-2 flex items-center gap-2">
<h4 className="font-medium">GPT-4 Turbo</h4>
<ToolsCallingIcon />
</div>
<p className="text-sm text-gray-600">
API
</p>
<div className="mt-2 flex gap-2">
<span className="rounded bg-green-100 px-2 py-1 text-xs text-green-800"></span>
<span className="rounded bg-blue-100 px-2 py-1 text-xs text-blue-800"></span>
</div>
</div>
<div className="rounded-lg border border-gray-200 p-4">
<div className="mb-2 flex items-center gap-2">
<h4 className="font-medium">Claude 3.5 Sonnet</h4>
<ToolsCallingIcon />
</div>
<p className="text-sm text-gray-600">
Anthropic的高性能模型使
</p>
<div className="mt-2 flex gap-2">
<span className="rounded bg-green-100 px-2 py-1 text-xs text-green-800"></span>
<span className="rounded bg-orange-100 px-2 py-1 text-xs text-orange-800"></span>
</div>
</div>
<div className="rounded-lg border border-gray-200 p-4">
<div className="mb-2 flex items-center gap-2">
<h4 className="font-medium">Llama 3.1 8B</h4>
{/* 不支持函数调用 */}
</div>
<p className="text-sm text-gray-600">
Meta的开源模型
</p>
<div className="mt-2 flex gap-2">
<span className="rounded bg-gray-100 px-2 py-1 text-xs text-gray-800"></span>
</div>
</div>
</div>
</div>
)
}
// Chat Message Context
export const ChatMessageContext: Story = {
render: () => (
<div className="space-y-4">
<h3 className="mb-3 font-semibold">使</h3>
<div className="space-y-3">
<div className="rounded-lg bg-blue-50 p-3">
<div className="mb-1 flex items-center gap-2 text-sm text-blue-800">
<ToolsCallingIcon iconClassName="w-3.5 h-3.5 mr-1 text-[#00b96b]" />
<span className="font-medium">调用工具: weather_api</span>
</div>
<p className="text-sm text-blue-700">
...
</p>
</div>
<div className="rounded-lg bg-green-50 p-3">
<div className="mb-1 flex items-center gap-2 text-sm text-green-800">
<ToolsCallingIcon iconClassName="w-3.5 h-3.5 mr-1 text-[#00b96b]" />
<span className="font-medium">调用工具: search_web</span>
</div>
<p className="text-sm text-green-700">
AI新闻...
</p>
</div>
<div className="rounded-lg bg-orange-50 p-3">
<div className="mb-1 flex items-center gap-2 text-sm text-orange-800">
<ToolsCallingIcon iconClassName="w-3.5 h-3.5 mr-1 text-[#00b96b]" />
<span className="font-medium">调用工具: code_interpreter</span>
</div>
<p className="text-sm text-orange-700">
Python代码计算结果...
</p>
</div>
</div>
</div>
)
}
// Tool Availability Indicator
export const ToolAvailabilityIndicator: Story = {
render: () => (
<div className="space-y-4">
<h3 className="mb-3 font-semibold"></h3>
<div className="rounded-lg border border-gray-200">
<div className="border-b border-gray-200 p-3">
<h4 className="font-medium text-gray-900"></h4>
</div>
<div className="divide-y divide-gray-200">
<div className="flex items-center justify-between p-3 hover:bg-gray-50">
<div className="flex items-center gap-2">
<ToolsCallingIcon iconClassName="w-4 h-4 mr-1.5 text-[#00b96b]" />
<span className="font-medium"></span>
</div>
<span className="rounded-full bg-green-100 px-2 py-1 text-xs text-green-800"></span>
</div>
<div className="flex items-center justify-between p-3 hover:bg-gray-50">
<div className="flex items-center gap-2">
<ToolsCallingIcon iconClassName="w-4 h-4 mr-1.5 text-[#00b96b]" />
<span className="font-medium"></span>
</div>
<span className="rounded-full bg-green-100 px-2 py-1 text-xs text-green-800"></span>
</div>
<div className="flex items-center justify-between p-3 hover:bg-gray-50 opacity-60">
<div className="flex items-center gap-2">
<ToolsCallingIcon iconClassName="w-4 h-4 mr-1.5 text-gray-400" />
<span className="font-medium"></span>
</div>
<span className="rounded-full bg-gray-100 px-2 py-1 text-xs text-gray-800"></span>
</div>
<div className="flex items-center justify-between p-3 hover:bg-gray-50">
<div className="flex items-center gap-2">
<ToolsCallingIcon iconClassName="w-4 h-4 mr-1.5 text-yellow-600" />
<span className="font-medium"></span>
</div>
<span className="rounded-full bg-yellow-100 px-2 py-1 text-xs text-yellow-800">使</span>
</div>
</div>
</div>
</div>
)
}
// Interactive Tool Selection
export const InteractiveToolSelection: Story = {
render: () => {
const tools = [
{ name: '天气查询', description: '获取实时天气信息', available: true },
{ name: '网络搜索', description: '搜索最新信息', available: true },
{ name: '代码执行', description: '运行Python代码', available: false },
{ name: '图像分析', description: '分析和描述图像', available: true },
{ name: '数据可视化', description: '创建图表和图形', available: false }
]
return (
<div className="space-y-4">
<h3 className="mb-3 font-semibold"></h3>
<div className="grid grid-cols-1 gap-3">
{tools.map((tool, index) => (
<button
key={index}
type="button"
className={`flex items-center gap-3 rounded-lg border p-3 text-left transition-all hover:shadow-md focus:outline-none focus:ring-2 focus:ring-blue-500/20 ${
tool.available
? 'border-gray-200 hover:border-blue-500'
: 'border-gray-200 opacity-60 cursor-not-allowed'
}`}
disabled={!tool.available}
>
<ToolsCallingIcon
iconClassName={`w-4 h-4 mr-1.5 ${
tool.available ? 'text-[#00b96b]' : 'text-gray-400'
}`}
/>
<div className="flex-1">
<div className="font-medium">{tool.name}</div>
<div className="text-sm text-gray-600">{tool.description}</div>
</div>
<div className="text-xs">
{tool.available ? (
<span className="rounded bg-green-100 px-2 py-1 text-green-800"></span>
) : (
<span className="rounded bg-gray-100 px-2 py-1 text-gray-800"></span>
)}
</div>
</button>
))}
</div>
</div>
)
}
}
// Loading Tool Calls
export const LoadingToolCalls: Story = {
render: () => (
<div className="space-y-4">
<h3 className="mb-3 font-semibold"></h3>
<div className="space-y-3">
<div className="rounded-lg border border-gray-200 p-3">
<div className="flex items-center gap-2">
<ToolsCallingIcon />
<span className="font-medium">...</span>
<div className="h-2 w-2 animate-pulse rounded-full bg-green-500"></div>
</div>
<p className="mt-1 text-sm text-gray-600">weather_api(city="北京")</p>
</div>
<div className="rounded-lg border border-green-200 bg-green-50 p-3">
<div className="flex items-center gap-2">
<ToolsCallingIcon iconClassName="w-4 h-4 mr-1.5 text-green-600" />
<span className="font-medium text-green-800"></span>
<span className="text-green-600"></span>
</div>
<p className="mt-1 text-sm text-green-700">
22°C
</p>
</div>
<div className="rounded-lg border border-red-200 bg-red-50 p-3">
<div className="flex items-center gap-2">
<ToolsCallingIcon iconClassName="w-4 h-4 mr-1.5 text-red-600" />
<span className="font-medium text-red-800"></span>
<span className="text-red-600"></span>
</div>
<p className="mt-1 text-sm text-red-700">
API密钥无效
</p>
</div>
</div>
</div>
)
}
// Settings Panel
export const SettingsPanel: Story = {
render: () => (
<div className="space-y-4">
<h3 className="mb-3 font-semibold">使</h3>
<div className="rounded-lg border border-gray-200 p-4">
<div className="mb-4 flex items-center gap-2">
<ToolsCallingIcon />
<h4 className="font-medium"></h4>
</div>
<div className="space-y-3">
<div className="flex items-center justify-between">
<div>
<div className="font-medium"></div>
<div className="text-sm text-gray-600">AI模型调用外部工具</div>
</div>
<input type="checkbox" className="rounded" defaultChecked />
</div>
<div className="flex items-center justify-between">
<div>
<div className="font-medium"></div>
<div className="text-sm text-gray-600"></div>
</div>
<input type="checkbox" className="rounded" />
</div>
<div className="flex items-center justify-between">
<div>
<div className="font-medium"></div>
<div className="text-sm text-gray-600"></div>
</div>
<input type="checkbox" className="rounded" defaultChecked />
</div>
</div>
</div>
</div>
)
}

View File

@ -1,4 +1,4 @@
import CustomCollapse from '@renderer/components/CustomCollapse' import { CustomCollapse } from '@cherrystudio/ui'
import { DynamicVirtualList, type DynamicVirtualListRef } from '@renderer/components/VirtualList' import { DynamicVirtualList, type DynamicVirtualListRef } from '@renderer/components/VirtualList'
import type { Model } from '@renderer/types' import type { Model } from '@renderer/types'
import type { ModelWithStatus } from '@renderer/types/healthCheck' import type { ModelWithStatus } from '@renderer/types/healthCheck'
@ -47,6 +47,7 @@ const ModelListGroup: React.FC<ModelListGroupProps> = ({
return ( return (
<CustomCollapseWrapper> <CustomCollapseWrapper>
<CustomCollapse <CustomCollapse
variant="shadow"
defaultActiveKey={defaultOpen ? ['1'] : []} defaultActiveKey={defaultOpen ? ['1'] : []}
onChange={handleCollapseChange} onChange={handleCollapseChange}
label={ label={
@ -69,9 +70,7 @@ const ModelListGroup: React.FC<ModelListGroupProps> = ({
</Tooltip> </Tooltip>
} }
styles={{ styles={{
header: { trigger: 'p-[3px_calc(6px_+_var(--scrollbar-width))_3px_16px]'
padding: '3px calc(6px + var(--scrollbar-width)) 3px 16px'
}
}}> }}>
<DynamicVirtualList <DynamicVirtualList
ref={listRef} ref={listRef}
@ -115,7 +114,8 @@ const CustomCollapseWrapper = styled.div`
/* 移除 collapse 的 padding转而在 scroller 内部调整 */ /* 移除 collapse 的 padding转而在 scroller 内部调整 */
.ant-collapse-content-box { .ant-collapse-content-box {
padding: 0 !important; padding: 0 !important;
} }import { classNames } from '../../../../utils/style';
` `
export default memo(ModelListGroup) export default memo(ModelListGroup)

View File

@ -2653,6 +2653,7 @@ __metadata:
dependencies: dependencies:
"@heroui/react": "npm:^2.8.4" "@heroui/react": "npm:^2.8.4"
"@storybook/addon-docs": "npm:^9.1.6" "@storybook/addon-docs": "npm:^9.1.6"
"@storybook/addon-themes": "npm:^9.1.6"
"@storybook/react-vite": "npm:^9.1.6" "@storybook/react-vite": "npm:^9.1.6"
"@types/react": "npm:^19.0.12" "@types/react": "npm:^19.0.12"
"@types/react-dom": "npm:^19.0.4" "@types/react-dom": "npm:^19.0.4"
@ -11842,6 +11843,17 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@storybook/addon-themes@npm:^9.1.6":
version: 9.1.6
resolution: "@storybook/addon-themes@npm:9.1.6"
dependencies:
ts-dedent: "npm:^2.0.0"
peerDependencies:
storybook: ^9.1.6
checksum: 10c0/3d5e17e19af70aee021537fcbf8782558de9abd85d96d813f66a510e217a58d1312af3e7aaf926f0f33eabf93d3ef2133581dd7e1f5fc28a2c5693ae7f33b5aa
languageName: node
linkType: hard
"@storybook/builder-vite@npm:9.1.6": "@storybook/builder-vite@npm:9.1.6":
version: 9.1.6 version: 9.1.6
resolution: "@storybook/builder-vite@npm:9.1.6" resolution: "@storybook/builder-vite@npm:9.1.6"