chore(ui): update package.json and migration status files

- Reformatted keywords and files array in package.json for better readability.
- Updated migration status to reflect the migration of additional components, increasing the total migrated count to 46 and reducing pending migrations to 190.
- Added new components to the migration status table, including ErrorBoundary and ProviderAvatar, while removing deprecated components like ErrorTag, SuccessTag, and WarnTag.
This commit is contained in:
MyPrototypeWhat 2025-09-16 13:31:38 +08:00
parent ffe897d58c
commit 8cc6b08831
39 changed files with 1108 additions and 4348 deletions

View File

@ -49,9 +49,9 @@ function MyComponent() {
## 迁移概览
- **总组件数**: 236
- **已迁移**: 43
- **已重构**: 0
- **待迁移**: 193
- **已迁移**: 46
- **已重构**: 2
- **待迁移**: 190
## 组件状态表
@ -62,12 +62,11 @@ function MyComponent() {
| | CustomTag | ✅ | ❌ | 自定义标签 |
| | DividerWithText | ✅ | ❌ | 带文本的分隔线 |
| | EmojiIcon | ✅ | ❌ | 表情图标 |
| | ErrorTag | ✅ | ❌ | 错误标签 |
| | ErrorBoundary | ✅ | ❌ | 错误边界 (通过 props 解耦) |
| | StatusTag | ✅ | ✅ | 统一状态标签(合并了 ErrorTag、SuccessTag、WarnTag、InfoTag|
| | IndicatorLight | ✅ | ❌ | 指示灯 |
| | Spinner | ✅ | ❌ | 加载动画 |
| | SuccessTag | ✅ | ❌ | 成功标签 |
| | TextBadge | ✅ | ❌ | 文本徽标 |
| | WarnTag | ✅ | ❌ | 警告标签 |
| | CustomCollapse | ✅ | ❌ | 自定义折叠面板 |
| **display** | | | | 显示组件 |
| | Ellipsis | ✅ | ❌ | 文本省略 |
@ -76,6 +75,7 @@ function MyComponent() {
| | EmojiAvatar | ✅ | ❌ | 表情头像 |
| | ListItem | ✅ | ❌ | 列表项 |
| | MaxContextCount | ✅ | ❌ | 最大上下文数显示 |
| | ProviderAvatar | ✅ | ❌ | 提供者头像 |
| | CodeViewer | ❌ | ❌ | 代码查看器 (外部依赖) |
| | OGCard | ❌ | ❌ | OG 卡片 |
| | MarkdownShadowDOMRenderer | ❌ | ❌ | Markdown 渲染器 |
@ -87,22 +87,11 @@ function MyComponent() {
| | Tab/* | ❌ | ❌ | 标签页 (Redux 依赖) |
| | TopView | ❌ | ❌ | 顶部视图 (window.api 依赖) |
| **icons** | | | | 图标组件 |
| | CopyIcon | ✅ | ❌ | 复制图标 |
| | DeleteIcon | ✅ | ❌ | 删除图标 |
| | EditIcon | ✅ | ❌ | 编辑图标 |
| | FileIcons | ✅ | ❌ | 文件图标 (包含 FileSvgIcon、FilePngIcon) |
| | Icon | ✅ | ✅ | 图标工厂函数和预定义图标(合并了 CopyIcon、DeleteIcon、EditIcon、RefreshIcon、ResetIcon、ToolIcon、VisionIcon、WebSearchIcon、WrapIcon、UnWrapIcon、OcrIcon|
| | FileIcons | ✅ | ❌ | 文件图标 (FileSvgIcon、FilePngIcon) |
| | ReasoningIcon | ✅ | ❌ | 推理图标 |
| | RefreshIcon | ✅ | ❌ | 刷新图标 |
| | ResetIcon | ✅ | ❌ | 重置图标 |
| | SvgSpinners180Ring | ✅ | ❌ | 旋转加载图标 |
| | ToolsCallingIcon | ✅ | ❌ | 工具调用图标 |
| | VisionIcon | ✅ | ❌ | 视觉图标 |
| | WebSearchIcon | ✅ | ❌ | 网页搜索图标 |
| | WrapIcon | ✅ | ❌ | 换行图标 |
| | UnWrapIcon | ✅ | ❌ | 不换行图标 |
| | OcrIcon | ✅ | ❌ | OCR 图标 |
| | ToolIcon | ✅ | ❌ | 工具图标 |
| | Other icons | ❌ | ❌ | 其他图标文件 |
| **interactive** | | | | 交互组件 |
| | InfoTooltip | ✅ | ❌ | 信息提示 |
| | HelpTooltip | ✅ | ❌ | 帮助提示 |
@ -112,6 +101,7 @@ function MyComponent() {
| | CollapsibleSearchBar | ✅ | ❌ | 可折叠搜索栏 |
| | ImageToolButton | ✅ | ❌ | 图片工具按钮 |
| | DraggableList | ✅ | ❌ | 可拖拽列表 |
| | CodeEditor | ✅ | ❌ | 代码编辑器 |
| | EmojiPicker | ❌ | ❌ | 表情选择器 (useTheme 依赖) |
| | Selector | ✅ | ❌ | 选择器 (i18n 依赖) |
| | ModelSelector | ❌ | ❌ | 模型选择器 (Redux 依赖) |
@ -122,7 +112,6 @@ function MyComponent() {
| **未分类** | | | | 需要分类的组件 |
| | Popups/* (16+ 文件) | ❌ | ❌ | 弹窗组件 (业务耦合) |
| | RichEditor/* (30+ 文件) | ❌ | ❌ | 富文本编辑器 |
| | CodeEditor/* | ❌ | ❌ | 代码编辑器 |
| | MarkdownEditor/* | ❌ | ❌ | Markdown 编辑器 |
| | MinApp/* | ❌ | ❌ | 迷你应用 (Redux 依赖) |
| | Avatar/* | ❌ | ❌ | 头像组件 |

View File

@ -48,9 +48,9 @@ When submitting PRs, please place components in the correct directory based on t
## Migration Overview
- **Total Components**: 236
- **Migrated**: 43
- **Migrated**: 46
- **Refactored**: 0
- **Pending Migration**: 193
- **Pending Migration**: 190
## Component Status Table
@ -61,6 +61,7 @@ When submitting PRs, please place components in the correct directory based on t
| | CustomTag | ✅ | ❌ | Custom tag |
| | DividerWithText | ✅ | ❌ | Divider with text |
| | EmojiIcon | ✅ | ❌ | Emoji icon |
| | ErrorBoundary | ✅ | ❌ | Error boundary (decoupled via props) |
| | ErrorTag | ✅ | ❌ | Error tag |
| | IndicatorLight | ✅ | ❌ | Indicator light |
| | Spinner | ✅ | ❌ | Loading spinner |
@ -75,6 +76,7 @@ When submitting PRs, please place components in the correct directory based on t
| | EmojiAvatar | ✅ | ❌ | Emoji avatar |
| | ListItem | ✅ | ❌ | List item |
| | MaxContextCount | ✅ | ❌ | Max context count display |
| | ProviderAvatar | ✅ | ❌ | Provider avatar |
| | CodeViewer | ❌ | ❌ | Code viewer (external deps) |
| | OGCard | ❌ | ❌ | OG card |
| | MarkdownShadowDOMRenderer | ❌ | ❌ | Markdown renderer |
@ -111,6 +113,7 @@ When submitting PRs, please place components in the correct directory based on t
| | CollapsibleSearchBar | ✅ | ❌ | Collapsible search bar |
| | ImageToolButton | ✅ | ❌ | Image tool button |
| | DraggableList | ✅ | ❌ | Draggable list |
| | CodeEditor | ✅ | ❌ | Code editor |
| | EmojiPicker | ❌ | ❌ | Emoji picker (useTheme dependency) |
| | Selector | ✅ | ❌ | Selector (i18n dependency) |
| | ModelSelector | ❌ | ❌ | Model selector (Redux dependency) |
@ -121,7 +124,6 @@ When submitting PRs, please place components in the correct directory based on t
| **Uncategorized** | | | | Components needing categorization |
| | Popups/* (16+ files) | ❌ | ❌ | Popup components (business coupled) |
| | RichEditor/* (30+ files) | ❌ | ❌ | Rich text editor |
| | CodeEditor/* | ❌ | ❌ | Code editor |
| | MarkdownEditor/* | ❌ | ❌ | Markdown editor |
| | MinApp/* | ❌ | ❌ | Mini app (Redux dependency) |
| | Avatar/* | ❌ | ❌ | Avatar components |

View File

@ -15,8 +15,7 @@
"lint": "eslint src --ext .ts,.tsx --fix",
"type-check": "tsc --noEmit",
"storybook": "storybook dev -p 6006",
"build-storybook": "storybook build",
"update:languages": "tsx scripts/update-languages.ts"
"build-storybook": "storybook build"
},
"keywords": [
"ui",

View File

@ -1,135 +0,0 @@
import { exec } from 'child_process'
import * as fs from 'fs/promises'
import * as linguistLanguages from 'linguist-languages'
import * as path from 'path'
import { promisify } from 'util'
const execAsync = promisify(exec)
type LanguageData = {
type: string
aliases?: string[]
extensions?: string[]
}
const LANGUAGES_FILE_PATH = path.join(__dirname, '../src/config/languages.ts')
/**
* Extracts and filters necessary language data from the linguist-languages package.
* @returns A record of language data.
*/
function extractAllLanguageData(): Record<string, LanguageData> {
console.log('🔍 Extracting language data from linguist-languages...')
const languages = Object.entries(linguistLanguages).reduce(
(acc, [name, langData]) => {
const { type, extensions, aliases } = langData as any
// Only include languages with extensions or aliases
if ((extensions && extensions.length > 0) || (aliases && aliases.length > 0)) {
acc[name] = {
type: type || 'programming',
...(extensions && { extensions }),
...(aliases && { aliases })
}
}
return acc
},
{} as Record<string, LanguageData>
)
console.log(`✅ Extracted ${Object.keys(languages).length} languages.`)
return languages
}
/**
* Generates the content for the languages.ts file.
* @param languages The language data to include in the file.
* @returns The generated file content as a string.
*/
function generateLanguagesFileContent(languages: Record<string, LanguageData>): string {
console.log('📝 Generating languages.ts file content...')
const sortedLanguages = Object.fromEntries(Object.entries(languages).sort(([a], [b]) => a.localeCompare(b)))
const languagesObjectString = JSON.stringify(sortedLanguages, null, 2)
const content = `/**
* Code language list.
* Data source: linguist-languages
*
*
* THIS FILE IS AUTOMATICALLY GENERATED BY A SCRIPT. DO NOT EDIT IT MANUALLY!
* Run \`yarn update:languages\` to update this file.
*
*
*/
type LanguageData = {
type: string;
aliases?: string[];
extensions?: string[];
};
export const languages: Record<string, LanguageData> = ${languagesObjectString};
`
console.log('✅ File content generated.')
return content
}
/**
* Formats a file using Prettier.
* @param filePath The path to the file to format.
*/
async function formatWithPrettier(filePath: string): Promise<void> {
console.log('🎨 Formatting file with Prettier...')
try {
await execAsync(`yarn prettier --write ${filePath}`)
console.log('✅ Prettier formatting complete.')
} catch (e: any) {
console.error('❌ Prettier formatting failed:', e.stdout || e.stderr)
throw new Error('Prettier formatting failed.')
}
}
/**
* Checks a file with TypeScript compiler.
* @param filePath The path to the file to check.
*/
async function checkTypeScript(filePath: string): Promise<void> {
console.log('🧐 Checking file with TypeScript compiler...')
try {
await execAsync(`yarn tsc --noEmit --skipLibCheck ${filePath}`)
console.log('✅ TypeScript check passed.')
} catch (e: any) {
console.error('❌ TypeScript check failed:', e.stdout || e.stderr)
throw new Error('TypeScript check failed.')
}
}
/**
* Main function to update the languages.ts file.
*/
async function updateLanguagesFile(): Promise<void> {
console.log('🚀 Starting to update languages.ts...')
try {
const extractedLanguages = extractAllLanguageData()
const fileContent = generateLanguagesFileContent(extractedLanguages)
await fs.writeFile(LANGUAGES_FILE_PATH, fileContent, 'utf-8')
console.log(`✅ Successfully wrote to ${LANGUAGES_FILE_PATH}`)
await formatWithPrettier(LANGUAGES_FILE_PATH)
await checkTypeScript(LANGUAGES_FILE_PATH)
console.log('🎉 Successfully updated languages.ts file!')
console.log(`📊 Contains ${Object.keys(extractedLanguages).length} languages.`)
} catch (error) {
console.error('❌ An error occurred during the update process:', (error as Error).message)
// No need to restore backup as we write only at the end of successful generation.
process.exit(1)
}
}
if (require.main === module) {
updateLanguagesFile()
}
export { updateLanguagesFile }

View File

@ -0,0 +1,101 @@
// Original path: src/renderer/src/components/ErrorBoundary.tsx
import { Button } from '@heroui/button'
import { Alert, Space } from 'antd'
import { ComponentType, ReactNode } from 'react'
import { ErrorBoundary, FallbackProps } from 'react-error-boundary'
import styled from 'styled-components'
import { formatErrorMessage } from './utils'
interface CustomFallbackProps extends FallbackProps {
onDebugClick?: () => void | Promise<void>
onReloadClick?: () => void | Promise<void>
debugButtonText?: string
reloadButtonText?: string
errorMessage?: string
}
const DefaultFallback: ComponentType<CustomFallbackProps> = (props: CustomFallbackProps): ReactNode => {
const {
error,
onDebugClick,
onReloadClick,
debugButtonText = 'Open DevTools',
reloadButtonText = 'Reload',
errorMessage = 'An error occurred'
} = props
return (
<ErrorContainer>
<Alert
message={errorMessage}
showIcon
description={formatErrorMessage(error)}
type="error"
action={
<Space>
{onDebugClick && (
<Button size="sm" onPress={onDebugClick}>
{debugButtonText}
</Button>
)}
{onReloadClick && (
<Button size="sm" onPress={onReloadClick}>
{reloadButtonText}
</Button>
)}
</Space>
}
/>
</ErrorContainer>
)
}
interface ErrorBoundaryCustomizedProps {
children: ReactNode
fallbackComponent?: ComponentType<CustomFallbackProps>
onDebugClick?: () => void | Promise<void>
onReloadClick?: () => void | Promise<void>
debugButtonText?: string
reloadButtonText?: string
errorMessage?: string
}
const ErrorBoundaryCustomized = ({
children,
fallbackComponent,
onDebugClick,
onReloadClick,
debugButtonText,
reloadButtonText,
errorMessage
}: ErrorBoundaryCustomizedProps) => {
const FallbackComponent = fallbackComponent ?? DefaultFallback
return (
<ErrorBoundary
FallbackComponent={(props: FallbackProps) => (
<FallbackComponent
{...props}
onDebugClick={onDebugClick}
onReloadClick={onReloadClick}
debugButtonText={debugButtonText}
reloadButtonText={reloadButtonText}
errorMessage={errorMessage}
/>
)}>
{children}
</ErrorBoundary>
)
}
const ErrorContainer = styled.div`
display: flex;
justify-content: center;
align-items: center;
width: 100%;
padding: 8px;
`
export { ErrorBoundaryCustomized as ErrorBoundary }
export type { ErrorBoundaryCustomizedProps, CustomFallbackProps }

View File

@ -0,0 +1,8 @@
// Utility functions for ErrorBoundary component
export function formatErrorMessage(error: Error): string {
if (error.message) {
return error.message
}
return error.toString()
}

View File

@ -1,17 +0,0 @@
// Original path: src/renderer/src/components/Tags/ErrorTag.tsx
import { CircleXIcon } from 'lucide-react'
import CustomTag from '../CustomTag'
type Props = {
iconSize?: number
message: string
}
export const ErrorTag = ({ iconSize: size = 14, message }: Props) => {
return (
<CustomTag icon={<CircleXIcon size={size} color="var(--color-status-error)" />} color="var(--color-status-error)">
{message}
</CustomTag>
)
}

View File

@ -0,0 +1,51 @@
import { AlertTriangleIcon, CheckIcon, CircleXIcon, InfoIcon, LucideIcon } from 'lucide-react'
import React from 'react'
import CustomTag from '../CustomTag'
export type StatusType = 'success' | 'error' | 'warning' | 'info'
export interface StatusTagProps {
type: StatusType
message: string
iconSize?: number
icon?: React.ReactNode
color?: string
}
const statusConfig: Record<StatusType, { Icon: LucideIcon; color: string }> = {
success: { Icon: CheckIcon, color: '#10B981' }, // green-500
error: { Icon: CircleXIcon, color: '#EF4444' }, // red-500
warning: { Icon: AlertTriangleIcon, color: '#F59E0B' }, // amber-500
info: { Icon: InfoIcon, color: '#3B82F6' } // blue-500
}
export const StatusTag: React.FC<StatusTagProps> = ({ type, message, iconSize = 14, icon, color }) => {
const config = statusConfig[type]
const Icon = config.Icon
const finalColor = color || config.color
const finalIcon = icon || <Icon size={iconSize} color={finalColor} />
return (
<CustomTag icon={finalIcon} color={finalColor}>
{message}
</CustomTag>
)
}
// 保留原有的导出以保持向后兼容
export const SuccessTag = ({ iconSize, message }: { iconSize?: number; message: string }) => (
<StatusTag type="success" iconSize={iconSize} message={message} />
)
export const ErrorTag = ({ iconSize, message }: { iconSize?: number; message: string }) => (
<StatusTag type="error" iconSize={iconSize} message={message} />
)
export const WarnTag = ({ iconSize, message }: { iconSize?: number; message: string }) => (
<StatusTag type="warning" iconSize={iconSize} message={message} />
)
export const InfoTag = ({ iconSize, message }: { iconSize?: number; message: string }) => (
<StatusTag type="info" iconSize={iconSize} message={message} />
)

View File

@ -1,17 +0,0 @@
// Original path: src/renderer/src/components/Tags/SuccessTag.tsx
import { CheckIcon } from 'lucide-react'
import CustomTag from '../CustomTag'
type Props = {
iconSize?: number
message: string
}
export const SuccessTag = ({ iconSize: size = 14, message }: Props) => {
return (
<CustomTag icon={<CheckIcon size={size} color="var(--color-status-success)" />} color="var(--color-status-success)">
{message}
</CustomTag>
)
}

View File

@ -1,19 +0,0 @@
// Original path: src/renderer/src/components/Tags/WarnTag.tsx
import { AlertTriangleIcon } from 'lucide-react'
import CustomTag from '../CustomTag'
type Props = {
iconSize?: number
message: string
}
export const WarnTag = ({ iconSize: size = 14, message }: Props) => {
return (
<CustomTag
icon={<AlertTriangleIcon size={size} color="var(--color-status-warning)" />}
color="var(--color-status-warning)">
{message}
</CustomTag>
)
}

View File

@ -0,0 +1,86 @@
// Original path: src/renderer/src/components/ProviderAvatar.tsx
import { Avatar } from 'antd'
import React from 'react'
import styled from 'styled-components'
import { generateColorFromChar, getFirstCharacter, getForegroundColor } from './utils'
interface ProviderAvatarProps {
providerId: string
providerName: string
logoSrc?: string
size?: number
className?: string
style?: React.CSSProperties
renderCustomLogo?: (providerId: string) => React.ReactNode
}
const ProviderLogo = styled(Avatar)`
width: 100%;
height: 100%;
border: 0.5px solid var(--color-border);
`
const ProviderSvgLogo = styled.div`
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
border: 0.5px solid var(--color-border);
border-radius: 100%;
& > svg {
width: 80%;
height: 80%;
}
`
export const ProviderAvatar: React.FC<ProviderAvatarProps> = ({
providerId,
providerName,
logoSrc,
size,
className,
style,
renderCustomLogo
}) => {
// Check if custom logo renderer is provided for special providers
if (renderCustomLogo) {
const customLogo = renderCustomLogo(providerId)
if (customLogo) {
return (
<ProviderSvgLogo className={className} style={style}>
{customLogo}
</ProviderSvgLogo>
)
}
}
// If logo source is provided, render image avatar
if (logoSrc) {
return (
<ProviderLogo draggable="false" shape="circle" src={logoSrc} className={className} style={style} size={size} />
)
}
// Default: generate avatar with first character and background color
const backgroundColor = generateColorFromChar(providerName)
const color = providerName ? getForegroundColor(backgroundColor) : 'white'
return (
<ProviderLogo
size={size}
shape="circle"
className={className}
style={{
backgroundColor,
color,
...style
}}>
{getFirstCharacter(providerName)}
</ProviderLogo>
)
}
export default ProviderAvatar

View File

@ -0,0 +1,37 @@
// Utility functions for ProviderAvatar component
export function generateColorFromChar(char: string): string {
const seed = char.charCodeAt(0)
const a = 1664525
const c = 1013904223
const m = Math.pow(2, 32)
let r = (a * seed + c) % m
let g = (a * r + c) % m
let b = (a * g + c) % m
r = Math.floor((r / m) * 256)
g = Math.floor((g / m) * 256)
b = Math.floor((b / m) * 256)
const toHex = (n: number) => n.toString(16).padStart(2, '0')
return `#${toHex(r)}${toHex(g)}${toHex(b)}`
}
export function getFirstCharacter(str: string): string {
for (const char of str) {
return char
}
return ''
}
export function getForegroundColor(backgroundColor: string): string {
// Simple luminance calculation
const hex = backgroundColor.replace('#', '')
const r = parseInt(hex.substring(0, 2), 16) / 255
const g = parseInt(hex.substring(2, 4), 16) / 255
const b = parseInt(hex.substring(4, 6), 16) / 255
const luminance = 0.2126 * r + 0.7152 * g + 0.0722 * b
return luminance > 0.179 ? '#000000' : '#FFFFFF'
}

View File

@ -1,6 +0,0 @@
// Original path: src/renderer/src/components/Icons/CopyIcon.tsx
import { Copy } from 'lucide-react'
const CopyIcon = (props: React.ComponentProps<typeof Copy>) => <Copy size="1rem" {...props} />
export default CopyIcon

View File

@ -1,6 +0,0 @@
// Original path: src/renderer/src/components/Icons/DeleteIcon.tsx
import { Trash } from 'lucide-react'
const DeleteIcon = (props: React.ComponentProps<typeof Trash>) => <Trash size="1rem" {...props} />
export default DeleteIcon

View File

@ -1,6 +0,0 @@
// Original path: src/renderer/src/components/Icons/EditIcon.tsx
import { Pencil } from 'lucide-react'
const EditIcon = (props: React.ComponentProps<typeof Pencil>) => <Pencil size="1rem" {...props} />
export default EditIcon

View File

@ -0,0 +1,41 @@
import {
AlignLeft,
Copy,
Eye,
LucideIcon,
Pencil,
RefreshCw,
RotateCcw,
ScanLine,
Search,
Trash,
WrapText,
Wrench
} from 'lucide-react'
import React from 'react'
// 创建一个 Icon 工厂函数
export function createIcon(IconComponent: LucideIcon, defaultSize: string | number = '1rem') {
const Icon = React.forwardRef<SVGSVGElement, React.ComponentProps<typeof IconComponent>>(
(props, ref) => <IconComponent ref={ref} size={defaultSize} {...props} />
)
Icon.displayName = `Icon(${IconComponent.displayName || IconComponent.name})`
return Icon
}
// 预定义的常用图标(向后兼容,只导入需要的图标)
export const CopyIcon = createIcon(Copy)
export const DeleteIcon = createIcon(Trash)
export const EditIcon = createIcon(Pencil)
export const RefreshIcon = createIcon(RefreshCw)
export const ResetIcon = createIcon(RotateCcw)
export const ToolIcon = createIcon(Wrench)
export const VisionIcon = createIcon(Eye)
export const WebSearchIcon = createIcon(Search)
export const WrapIcon = createIcon(WrapText)
export const UnWrapIcon = createIcon(AlignLeft)
export const OcrIcon = createIcon(ScanLine)
// 导出 createIcon 以便用户自行创建图标组件
export type { LucideIcon }
export type { LucideProps } from 'lucide-react'

View File

@ -1,8 +0,0 @@
// Original path: src/renderer/src/components/Icons/OcrIcon.tsx
import { FC } from 'react'
const OcrIcon: FC<React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement>> = (props) => {
return <i {...props} className={`iconfont icon-OCRshibie ${props.className}`} />
}
export default OcrIcon

View File

@ -1,6 +0,0 @@
// Original path: src/renderer/src/components/Icons/RefreshIcon.tsx
import { RefreshCw } from 'lucide-react'
const RefreshIcon = (props: React.ComponentProps<typeof RefreshCw>) => <RefreshCw size="1rem" {...props} />
export default RefreshIcon

View File

@ -1,6 +0,0 @@
// Original path: src/renderer/src/components/Icons/ResetIcon.tsx
import { RotateCcw } from 'lucide-react'
const ResetIcon = (props: React.ComponentProps<typeof RotateCcw>) => <RotateCcw size="1rem" {...props} />
export default ResetIcon

View File

@ -1,8 +0,0 @@
// Original path: src/renderer/src/components/Icons/ToolIcon.tsx
import { FC } from 'react'
const ToolIcon: FC<React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement>> = (props) => {
return <i {...props} className={`iconfont icon-plugin ${props.className}`} />
}
export default ToolIcon

View File

@ -1,18 +0,0 @@
// Original path: src/renderer/src/components/Icons/UnWrapIcon.tsx
const UnWrapIcon = (props: React.SVGProps<SVGSVGElement>) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width="1em"
height="1em"
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
className="unwrap_svg__lucide unwrap_svg__lucide-text unwrap_svg__size-4"
viewBox="0 0 24 24"
{...props}>
<path d="M17 6.1H3M21 12.1H3M15.1 18H3" />
</svg>
)
export default UnWrapIcon

View File

@ -1,31 +0,0 @@
// Original: src/renderer/src/components/Icons/VisionIcon.tsx
import { Tooltip } from 'antd'
import { ImageIcon } from 'lucide-react'
import React, { FC } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
const VisionIcon: FC<React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement>> = (props) => {
const { t } = useTranslation()
return (
<Container>
<Tooltip title={t('models.type.vision')} placement="top">
<Icon size={15} {...(props as any)} />
</Tooltip>
</Container>
)
}
const Container = styled.div`
display: flex;
justify-content: center;
align-items: center;
`
const Icon = styled(ImageIcon)`
color: var(--color-primary);
margin-right: 6px;
`
export default VisionIcon

View File

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

View File

@ -1,21 +0,0 @@
// Original path: src/renderer/src/components/Icons/WrapIcon.tsx
import React from 'react'
const WrapIcon = (props: React.SVGProps<SVGSVGElement>) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width="1em"
height="1em"
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
className="wrap_svg__lucide wrap_svg__lucide-wrap-text wrap_svg__size-4"
viewBox="0 0 24 24"
{...props}>
<path d="M3 6h18M3 12h15a3 3 0 1 1 0 6h-4" />
<path d="m16 16-2 2 2 2M3 18h7" />
</svg>
)
export default WrapIcon

View File

@ -4,12 +4,13 @@ export { default as CustomCollapse } from './base/CustomCollapse'
export { default as CustomTag } from './base/CustomTag'
export { default as DividerWithText } from './base/DividerWithText'
export { default as EmojiIcon } from './base/EmojiIcon'
export { ErrorTag } from './base/ErrorTag'
export { ErrorBoundary } from './base/ErrorBoundary'
export type { ErrorBoundaryCustomizedProps, CustomFallbackProps } from './base/ErrorBoundary'
export { default as IndicatorLight } from './base/IndicatorLight'
export { default as Spinner } from './base/Spinner'
export { SuccessTag } from './base/SuccessTag'
export { StatusTag, ErrorTag, SuccessTag, WarnTag, InfoTag } from './base/StatusTag'
export type { StatusType, StatusTagProps } from './base/StatusTag'
export { default as TextBadge } from './base/TextBadge'
export { WarnTag } from './base/WarnTag'
// Display Components
export { default as Ellipsis } from './display/Ellipsis'
@ -17,6 +18,7 @@ export { default as EmojiAvatar } from './display/EmojiAvatar'
export { default as ExpandableText } from './display/ExpandableText'
export { default as ListItem } from './display/ListItem'
export { default as MaxContextCount } from './display/MaxContextCount'
export { ProviderAvatar } from './display/ProviderAvatar'
export { default as ThinkingEffect } from './display/ThinkingEffect'
// Layout Components
@ -24,21 +26,25 @@ export { default as HorizontalScrollContainer } from './layout/HorizontalScrollC
export { default as Scrollbar } from './layout/Scrollbar'
// Icon Components
export { default as CopyIcon } from './icons/CopyIcon'
export { default as DeleteIcon } from './icons/DeleteIcon'
export { default as EditIcon } from './icons/EditIcon'
export {
createIcon,
CopyIcon,
DeleteIcon,
EditIcon,
RefreshIcon,
ResetIcon,
ToolIcon,
VisionIcon,
WebSearchIcon,
WrapIcon,
UnWrapIcon,
OcrIcon
} from './icons/Icon'
export type { LucideIcon, LucideProps } from './icons/Icon'
export { FilePngIcon, FileSvgIcon } from './icons/FileIcons'
export { default as OcrIcon } from './icons/OcrIcon'
export { default as ReasoningIcon } from './icons/ReasoningIcon'
export { default as RefreshIcon } from './icons/RefreshIcon'
export { default as ResetIcon } from './icons/ResetIcon'
export { default as SvgSpinners180Ring } from './icons/SvgSpinners180Ring'
export { default as ToolIcon } from './icons/ToolIcon'
export { default as ToolsCallingIcon } from './icons/ToolsCallingIcon'
export { default as UnWrapIcon } from './icons/UnWrapIcon'
export { default as VisionIcon } from './icons/VisionIcon'
export { default as WebSearchIcon } from './icons/WebSearchIcon'
export { default as WrapIcon } from './icons/WrapIcon'
// Interactive Components
export {

View File

@ -15,6 +15,7 @@ const CodeEditor = ({
value,
placeholder,
language,
languageConfig,
onSave,
onChange,
onBlur,
@ -55,7 +56,7 @@ const CodeEditor = ({
const initialContent = useRef(options?.stream ? (value ?? '').trimEnd() : (value ?? ''))
const editorViewRef = useRef<EditorView | null>(null)
const langExtensions = useLanguageExtensions(language, options?.lint)
const langExtensions = useLanguageExtensions(language, options?.lint, languageConfig)
const handleSave = useCallback(() => {
const currentDoc = editorViewRef.current?.state.doc.toString() ?? ''

View File

@ -3,6 +3,7 @@ import { EditorView } from '@codemirror/view'
import { Extension, keymap } from '@uiw/react-codemirror'
import { useEffect, useMemo, useState } from 'react'
import { LanguageConfig } from './types'
import { getNormalizedExtension } from './utils'
/** linter
@ -34,8 +35,8 @@ const specialLanguageLoaders: Record<string, () => Promise<Extension>> = {
/**
*
*/
async function loadLanguageExtension(language: string): Promise<Extension | null> {
const fileExt = await getNormalizedExtension(language)
async function loadLanguageExtension(language: string, languageConfig?: LanguageConfig): Promise<Extension | null> {
const fileExt = await getNormalizedExtension(language, languageConfig)
// 尝试加载特殊语言
const specialLoader = specialLanguageLoaders[fileExt]
@ -62,8 +63,8 @@ async function loadLanguageExtension(language: string): Promise<Extension | null
/**
* linter
*/
async function loadLinterExtension(language: string): Promise<Extension | null> {
const fileExt = await getNormalizedExtension(language)
async function loadLinterExtension(language: string, languageConfig?: LanguageConfig): Promise<Extension | null> {
const fileExt = await getNormalizedExtension(language, languageConfig)
const loader = linterLoaders[fileExt]
if (!loader) return null
@ -79,7 +80,7 @@ async function loadLinterExtension(language: string): Promise<Extension | null>
/**
*
*/
export const useLanguageExtensions = (language: string, lint?: boolean) => {
export const useLanguageExtensions = (language: string, lint?: boolean, languageConfig?: LanguageConfig) => {
const [extensions, setExtensions] = useState<Extension[]>([])
useEffect(() => {
@ -89,8 +90,8 @@ export const useLanguageExtensions = (language: string, lint?: boolean) => {
try {
// 加载所有扩展
const [languageResult, linterResult] = await Promise.allSettled([
loadLanguageExtension(language),
lint ? loadLinterExtension(language) : Promise.resolve(null)
loadLanguageExtension(language, languageConfig),
lint ? loadLinterExtension(language, languageConfig) : Promise.resolve(null)
])
if (cancelled) return
@ -121,7 +122,7 @@ export const useLanguageExtensions = (language: string, lint?: boolean) => {
return () => {
cancelled = true
}
}, [language, lint])
}, [language, lint, languageConfig])
return extensions
}

View File

@ -2,6 +2,16 @@ import { BasicSetupOptions, Extension } from '@uiw/react-codemirror'
export type CodeMirrorTheme = 'light' | 'dark' | 'none' | Extension
/** Language data structure for file extension mapping */
export interface LanguageData {
type: string
aliases?: string[]
extensions?: string[]
}
/** Language configuration mapping language names to their data */
export type LanguageConfig = Record<string, LanguageData>
export interface CodeEditorHandles {
save?: () => void
}
@ -20,6 +30,12 @@ export interface CodeEditorProps {
* - Supports file extensions: .cpp/cpp, .js/js, .py/py, etc.
*/
language: string
/**
* Language configuration for extension mapping.
* If not provided, will use a default minimal configuration.
* @optional
*/
languageConfig?: LanguageConfig
/** Fired when ref.save() is called or the save shortcut is triggered. */
onSave?: (newContent: string) => void
/** Fired when the editor content changes. */

View File

@ -2,8 +2,7 @@ import * as cmThemes from '@uiw/codemirror-themes-all'
import { Extension } from '@uiw/react-codemirror'
import diff from 'fast-diff'
import { getExtensionByLanguage } from '../../../utils/codeLanguage'
import { CodeMirrorTheme } from './types'
import { CodeMirrorTheme, LanguageConfig } from './types'
/**
* Computes code changes using fast-diff and converts them to CodeMirror changes.
@ -50,15 +49,158 @@ const _customLanguageExtensions: Record<string, string> = {
graphviz: 'dot'
}
// Default minimal language configuration for common languages
const _defaultLanguageConfig: LanguageConfig = {
JavaScript: {
type: 'programming',
extensions: ['.js', '.mjs', '.cjs'],
aliases: ['js', 'node']
},
TypeScript: {
type: 'programming',
extensions: ['.ts'],
aliases: ['ts']
},
Python: {
type: 'programming',
extensions: ['.py'],
aliases: ['python3', 'py']
},
Java: {
type: 'programming',
extensions: ['.java']
},
'C++': {
type: 'programming',
extensions: ['.cpp', '.cc', '.cxx'],
aliases: ['cpp']
},
C: {
type: 'programming',
extensions: ['.c']
},
'C#': {
type: 'programming',
extensions: ['.cs'],
aliases: ['csharp']
},
HTML: {
type: 'markup',
extensions: ['.html', '.htm']
},
CSS: {
type: 'markup',
extensions: ['.css']
},
JSON: {
type: 'data',
extensions: ['.json']
},
XML: {
type: 'data',
extensions: ['.xml']
},
YAML: {
type: 'data',
extensions: ['.yml', '.yaml']
},
SQL: {
type: 'data',
extensions: ['.sql']
},
Shell: {
type: 'programming',
extensions: ['.sh', '.bash'],
aliases: ['bash', 'sh']
},
Go: {
type: 'programming',
extensions: ['.go'],
aliases: ['golang']
},
Rust: {
type: 'programming',
extensions: ['.rs']
},
PHP: {
type: 'programming',
extensions: ['.php']
},
Ruby: {
type: 'programming',
extensions: ['.rb'],
aliases: ['rb']
},
Swift: {
type: 'programming',
extensions: ['.swift']
},
Kotlin: {
type: 'programming',
extensions: ['.kt']
},
Dart: {
type: 'programming',
extensions: ['.dart']
},
R: {
type: 'programming',
extensions: ['.r']
},
MATLAB: {
type: 'programming',
extensions: ['.m']
}
}
/**
* Get the file extension of the language, by language name
* - First, exact match
* - Then, case-insensitive match
* - Finally, match aliases
* If there are multiple file extensions, only the first one will be returned
* @param language language name
* @param languageConfig optional language configuration, defaults to a minimal config
* @returns file extension
*/
export function getExtensionByLanguage(language: string, languageConfig?: LanguageConfig): string {
const languages = languageConfig || _defaultLanguageConfig
const lowerLanguage = language.toLowerCase()
// Exact match language name
const directMatch = languages[language]
if (directMatch?.extensions?.[0]) {
return directMatch.extensions[0]
}
// Case-insensitive match language name
for (const [langName, data] of Object.entries(languages)) {
if (langName.toLowerCase() === lowerLanguage && data.extensions?.[0]) {
return data.extensions[0]
}
}
// Match aliases
for (const [, data] of Object.entries(languages)) {
if (data.aliases?.some((alias) => alias.toLowerCase() === lowerLanguage)) {
return data.extensions?.[0] || `.${language}`
}
}
// Fallback to language name
return `.${language}`
}
/**
* Get the file extension of the language, for @uiw/codemirror-extensions-langs
* - First, search for custom extensions
* - Then, search for github linguist extensions
* - Then, search for language configuration extensions
* - Finally, assume the name is already an extension
* @param language language name
* @param languageConfig optional language configuration
* @returns file extension (without `.` prefix)
*/
export async function getNormalizedExtension(language: string) {
export async function getNormalizedExtension(language: string, languageConfig?: LanguageConfig) {
let lang = language
// If the language name looks like an extension, remove the dot
@ -74,8 +216,8 @@ export async function getNormalizedExtension(language: string) {
return customExt
}
// 2. Search for github linguist extensions
const linguistExt = getExtensionByLanguage(lang)
// 2. Search for language configuration extensions
const linguistExt = getExtensionByLanguage(lang, languageConfig)
if (linguistExt) {
return linguistExt.slice(1)
}

File diff suppressed because it is too large Load Diff

View File

@ -1,39 +0,0 @@
import { languages } from '../config/languages'
/**
* Get the file extension of the language, by language name
* - First, exact match
* - Then, case-insensitive match
* - Finally, match aliases
* If there are multiple file extensions, only the first one will be returned
* @param language language name
* @returns file extension
*/
export function getExtensionByLanguage(language: string): string {
const lowerLanguage = language.toLowerCase()
// Exact match language name
const directMatch = languages[language]
if (directMatch?.extensions?.[0]) {
return directMatch.extensions[0]
}
const languageEntries = Object.entries(languages)
// Case-insensitive match language name
for (const [langName, data] of languageEntries) {
if (langName.toLowerCase() === lowerLanguage && data.extensions?.[0]) {
return data.extensions[0]
}
}
// Match aliases
for (const [, data] of languageEntries) {
if (data.aliases?.some((alias) => alias.toLowerCase() === lowerLanguage)) {
return data.extensions?.[0] || `.${language}`
}
}
// Fallback to language name
return `.${language}`
}

View File

@ -1,48 +0,0 @@
import type { Meta, StoryObj } from '@storybook/react-vite'
import { ErrorTag } from '../../../src/components/base/ErrorTag'
const meta: Meta<typeof ErrorTag> = {
title: 'Base/ErrorTag',
component: ErrorTag,
parameters: {
layout: 'centered'
},
tags: ['autodocs'],
argTypes: {
iconSize: { control: { type: 'range', min: 10, max: 20, step: 1 } },
message: { control: 'text' }
}
}
export default meta
type Story = StoryObj<typeof meta>
export const Default: Story = {
args: {
message: '错误信息'
}
}
export const ServerError: Story = {
args: {
message: '服务器连接失败'
}
}
export const ValidationError: Story = {
args: {
message: '数据验证失败',
iconSize: 16
}
}
export const Examples: Story = {
render: () => (
<div className="space-y-2">
<ErrorTag message="操作失败" />
<ErrorTag message="权限不足" />
<ErrorTag message="文件上传失败" iconSize={18} />
</div>
)
}

View File

@ -0,0 +1,176 @@
import type { Meta, StoryObj } from '@storybook/react'
import { ErrorTag, InfoTag, StatusTag, SuccessTag, WarnTag } from '../../../src/components/base/StatusTag'
const meta: Meta<typeof StatusTag> = {
title: 'Base/StatusTag',
component: StatusTag,
parameters: {
layout: 'centered'
},
tags: ['autodocs'],
argTypes: {
type: {
control: { type: 'select' },
options: ['success', 'error', 'warning', 'info']
},
iconSize: { control: { type: 'range', min: 10, max: 24, step: 1 } },
message: { control: 'text' },
color: { control: 'color' }
}
}
export default meta
type Story = StoryObj<typeof meta>
// Default
export const Default: Story = {
args: {
type: 'success',
message: 'Success'
}
}
// All Types
export const AllTypes: Story = {
render: () => (
<div className="flex flex-col gap-3">
<StatusTag type="success" message="Success message" />
<StatusTag type="error" message="Error message" />
<StatusTag type="warning" message="Warning message" />
<StatusTag type="info" message="Info message" />
</div>
)
}
// Convenience Components
export const ConvenienceComponents: Story = {
render: () => (
<div className="flex flex-col gap-3">
<SuccessTag message="Operation completed" />
<ErrorTag message="Operation failed" />
<WarnTag message="Please check this" />
<InfoTag message="Additional information" />
</div>
)
}
// Different Icon Sizes
export const IconSizes: Story = {
render: () => (
<div className="space-y-4">
<div className="flex items-center gap-4">
<StatusTag type="success" iconSize={10} message="Small icon" />
<StatusTag type="success" iconSize={14} message="Default icon" />
<StatusTag type="success" iconSize={18} message="Large icon" />
<StatusTag type="success" iconSize={24} message="Extra large icon" />
</div>
<div className="flex items-center gap-4">
<ErrorTag iconSize={10} message="Small icon" />
<ErrorTag iconSize={14} message="Default icon" />
<ErrorTag iconSize={18} message="Large icon" />
<ErrorTag iconSize={24} message="Extra large icon" />
</div>
</div>
)
}
// Custom Colors
export const CustomColors: Story = {
render: () => (
<div className="flex flex-col gap-3">
<StatusTag type="success" message="Custom purple" color="#8B5CF6" />
<StatusTag type="error" message="Custom blue" color="#3B82F6" />
<StatusTag type="warning" message="Custom green" color="#10B981" />
<StatusTag type="info" message="Custom pink" color="#EC4899" />
</div>
)
}
// In Context
export const InContext: Story = {
render: () => (
<div className="space-y-4">
<div className="rounded-lg border border-gray-200 p-4">
<h3 className="mb-2 font-semibold">Form Submission</h3>
<p className="mb-3 text-sm text-gray-600">Your form has been processed.</p>
<SuccessTag message="Form submitted successfully" />
</div>
<div className="rounded-lg border border-gray-200 p-4">
<h3 className="mb-2 font-semibold">Validation Error</h3>
<p className="mb-3 text-sm text-gray-600">Please fix the following issues:</p>
<ErrorTag message="Invalid email format" />
</div>
<div className="rounded-lg border border-gray-200 p-4">
<h3 className="mb-2 font-semibold">System Status</h3>
<div className="space-y-2">
<SuccessTag message="Database connected" />
<WarnTag message="High memory usage" />
<ErrorTag message="Email service down" />
<InfoTag message="Last backup: 2 hours ago" />
</div>
</div>
</div>
)
}
// Use Cases
export const UseCases: Story = {
render: () => (
<div className="grid grid-cols-2 gap-6">
<div className="space-y-2">
<h4 className="font-medium">Success States</h4>
<div className="space-y-2">
<SuccessTag message="Saved" />
<SuccessTag message="Published" />
<SuccessTag message="Deployed" />
<SuccessTag message="Verified" />
</div>
</div>
<div className="space-y-2">
<h4 className="font-medium">Error States</h4>
<div className="space-y-2">
<ErrorTag message="Failed" />
<ErrorTag message="Timeout" />
<ErrorTag message="Not found" />
<ErrorTag message="Access denied" />
</div>
</div>
<div className="space-y-2">
<h4 className="font-medium">Warning States</h4>
<div className="space-y-2">
<WarnTag message="Deprecated" />
<WarnTag message="Limited" />
<WarnTag message="Expiring soon" />
<WarnTag message="Low balance" />
</div>
</div>
<div className="space-y-2">
<h4 className="font-medium">Info States</h4>
<div className="space-y-2">
<InfoTag message="New" />
<InfoTag message="Beta" />
<InfoTag message="Preview" />
<InfoTag message="Optional" />
</div>
</div>
</div>
)
}
// Long Messages
export const LongMessages: Story = {
render: () => (
<div className="max-w-md space-y-3">
<SuccessTag message="Your request has been successfully processed and saved to the database" />
<ErrorTag message="Unable to connect to the server. Please check your network connection and try again" />
<WarnTag message="This feature will be deprecated in the next major version. Please migrate to the new API" />
<InfoTag message="Additional information about this feature can be found in the documentation at docs.example.com" />
</div>
)
}

View File

@ -1,110 +0,0 @@
import type { Meta, StoryObj } from '@storybook/react'
import { SuccessTag } from '../../../src/components/base/SuccessTag'
const meta: Meta<typeof SuccessTag> = {
title: 'Base/SuccessTag',
component: SuccessTag,
parameters: {
layout: 'centered'
},
tags: ['autodocs'],
argTypes: {
iconSize: { control: { type: 'range', min: 10, max: 24, step: 1 } },
message: { control: 'text' }
}
}
export default meta
type Story = StoryObj<typeof meta>
// Default
export const Default: Story = {
args: {
message: 'Success'
}
}
// Different Messages
export const DifferentMessages: Story = {
render: () => (
<div className="flex flex-col gap-3">
<SuccessTag message="Operation completed" />
<SuccessTag message="File saved successfully" />
<SuccessTag message="Data uploaded" />
<SuccessTag message="Connection established" />
<SuccessTag message="Task finished" />
</div>
)
}
// Different Icon Sizes
export const IconSizes: Story = {
render: () => (
<div className="flex items-center gap-4">
<SuccessTag iconSize={10} message="Small icon" />
<SuccessTag iconSize={14} message="Default icon" />
<SuccessTag iconSize={18} message="Large icon" />
<SuccessTag iconSize={24} message="Extra large icon" />
</div>
)
}
// In Context
export const InContext: Story = {
render: () => (
<div className="space-y-4">
<div className="rounded-lg border border-gray-200 p-4">
<h3 className="mb-2 font-semibold">Form Submission</h3>
<p className="mb-3 text-sm text-gray-600">Your form has been processed.</p>
<SuccessTag message="Form submitted successfully" />
</div>
<div className="rounded-lg border border-gray-200 p-4">
<h3 className="mb-2 font-semibold">File Upload</h3>
<div className="mb-2 space-y-2">
<div className="text-sm">document.pdf</div>
<div className="text-sm">image.png</div>
<div className="text-sm">data.csv</div>
</div>
<SuccessTag message="3 files uploaded" />
</div>
<div className="rounded-lg border border-gray-200 p-4">
<h3 className="mb-2 font-semibold">System Status</h3>
<div className="space-y-2">
<SuccessTag message="All systems operational" />
<SuccessTag message="Database connected" />
<SuccessTag message="API responding" />
</div>
</div>
</div>
)
}
// Use Cases
export const UseCases: Story = {
render: () => (
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<h4 className="font-medium">Actions</h4>
<div className="space-y-2">
<SuccessTag message="Saved" />
<SuccessTag message="Published" />
<SuccessTag message="Deployed" />
<SuccessTag message="Synced" />
</div>
</div>
<div className="space-y-2">
<h4 className="font-medium">States</h4>
<div className="space-y-2">
<SuccessTag message="Active" />
<SuccessTag message="Online" />
<SuccessTag message="Ready" />
<SuccessTag message="Verified" />
</div>
</div>
</div>
)
}

View File

@ -1,48 +0,0 @@
import type { Meta, StoryObj } from '@storybook/react-vite'
import { WarnTag } from '../../../src/components/base/WarnTag'
const meta: Meta<typeof WarnTag> = {
title: 'Base/WarnTag',
component: WarnTag,
parameters: {
layout: 'centered'
},
tags: ['autodocs'],
argTypes: {
iconSize: { control: { type: 'range', min: 10, max: 20, step: 1 } },
message: { control: 'text' }
}
}
export default meta
type Story = StoryObj<typeof meta>
export const Default: Story = {
args: {
message: '警告信息'
}
}
export const LongMessage: Story = {
args: {
message: '这是一个比较长的警告信息'
}
}
export const CustomIconSize: Story = {
args: {
message: '自定义图标大小',
iconSize: 18
}
}
export const Examples: Story = {
render: () => (
<div className="space-y-2">
<WarnTag message="表单验证失败" />
<WarnTag message="网络连接不稳定" />
<WarnTag message="存储空间不足" iconSize={16} />
</div>
)
}

View File

@ -0,0 +1,290 @@
import type { Meta, StoryObj } from '@storybook/react'
import {
Copy,
Trash,
Pencil,
RefreshCw,
RotateCcw,
Wrench,
Eye,
Search,
WrapText,
AlignLeft,
ScanLine,
Settings,
Download,
Upload,
ChevronRight
} from 'lucide-react'
import {
createIcon,
CopyIcon,
DeleteIcon,
EditIcon,
RefreshIcon,
ResetIcon,
ToolIcon,
VisionIcon,
WebSearchIcon,
WrapIcon,
UnWrapIcon,
OcrIcon
} from '../../../src/components/icons/Icon'
// Create a dummy component for the story
const IconShowcase = () => <div />
const meta: Meta<typeof IconShowcase> = {
title: 'Icons/Icon',
component: IconShowcase,
parameters: {
layout: 'centered'
},
tags: ['autodocs']
}
export default meta
type Story = StoryObj<typeof meta>
// Predefined Icons
export const PredefinedIcons: Story = {
render: () => (
<div className="space-y-6">
<div>
<h3 className="mb-3 font-semibold">Predefined Icons (Default Size: 1rem)</h3>
<div className="grid grid-cols-6 gap-4">
<div className="flex flex-col items-center gap-2">
<CopyIcon />
<span className="text-xs text-gray-600">CopyIcon</span>
</div>
<div className="flex flex-col items-center gap-2">
<DeleteIcon />
<span className="text-xs text-gray-600">DeleteIcon</span>
</div>
<div className="flex flex-col items-center gap-2">
<EditIcon />
<span className="text-xs text-gray-600">EditIcon</span>
</div>
<div className="flex flex-col items-center gap-2">
<RefreshIcon />
<span className="text-xs text-gray-600">RefreshIcon</span>
</div>
<div className="flex flex-col items-center gap-2">
<ResetIcon />
<span className="text-xs text-gray-600">ResetIcon</span>
</div>
<div className="flex flex-col items-center gap-2">
<ToolIcon />
<span className="text-xs text-gray-600">ToolIcon</span>
</div>
<div className="flex flex-col items-center gap-2">
<VisionIcon />
<span className="text-xs text-gray-600">VisionIcon</span>
</div>
<div className="flex flex-col items-center gap-2">
<WebSearchIcon />
<span className="text-xs text-gray-600">WebSearchIcon</span>
</div>
<div className="flex flex-col items-center gap-2">
<WrapIcon />
<span className="text-xs text-gray-600">WrapIcon</span>
</div>
<div className="flex flex-col items-center gap-2">
<UnWrapIcon />
<span className="text-xs text-gray-600">UnWrapIcon</span>
</div>
<div className="flex flex-col items-center gap-2">
<OcrIcon />
<span className="text-xs text-gray-600">OcrIcon</span>
</div>
</div>
</div>
</div>
)
}
// Different Sizes
export const DifferentSizes: Story = {
render: () => (
<div className="space-y-4">
<div className="flex items-center gap-4">
<CopyIcon size={12} />
<CopyIcon size={16} />
<CopyIcon size={20} />
<CopyIcon size={24} />
<CopyIcon size={32} />
<CopyIcon size={48} />
</div>
<div className="flex items-center gap-2 text-xs text-gray-600">
<span>12px</span>
<span className="ml-2">16px</span>
<span className="ml-4">20px</span>
<span className="ml-4">24px</span>
<span className="ml-6">32px</span>
<span className="ml-10">48px</span>
</div>
</div>
)
}
// Custom Colors
export const CustomColors: Story = {
render: () => (
<div className="flex items-center gap-4">
<EditIcon color="#3B82F6" size={24} />
<EditIcon color="#10B981" size={24} />
<EditIcon color="#F59E0B" size={24} />
<EditIcon color="#EF4444" size={24} />
<EditIcon color="#8B5CF6" size={24} />
<EditIcon color="#EC4899" size={24} />
</div>
)
}
// Custom Icon Creation
export const CustomIconCreation: Story = {
render: () => {
// Create custom icons using the factory
const SettingsIcon = createIcon(Settings, 24)
const DownloadIcon = createIcon(Download, 20)
const UploadIcon = createIcon(Upload, 20)
const ChevronIcon = createIcon(ChevronRight, 16)
return (
<div className="space-y-6">
<div>
<h3 className="mb-3 font-semibold">Custom Icons Created with Factory</h3>
<div className="flex items-center gap-6">
<div className="flex flex-col items-center gap-2">
<SettingsIcon />
<span className="text-xs text-gray-600">Settings (24px)</span>
</div>
<div className="flex flex-col items-center gap-2">
<DownloadIcon />
<span className="text-xs text-gray-600">Download (20px)</span>
</div>
<div className="flex flex-col items-center gap-2">
<UploadIcon />
<span className="text-xs text-gray-600">Upload (20px)</span>
</div>
<div className="flex flex-col items-center gap-2">
<ChevronIcon />
<span className="text-xs text-gray-600">Chevron (16px)</span>
</div>
</div>
</div>
<div>
<h3 className="mb-3 font-semibold">Override Default Size</h3>
<div className="flex items-center gap-4">
<SettingsIcon size={32} />
<DownloadIcon size={32} />
<UploadIcon size={32} />
<ChevronIcon size={32} />
</div>
</div>
</div>
)
}
}
// Icon States
export const IconStates: Story = {
render: () => (
<div className="space-y-4">
<div className="flex items-center gap-4">
<button className="rounded p-2 hover:bg-gray-100">
<EditIcon size={20} />
</button>
<button className="rounded p-2 hover:bg-gray-100" disabled>
<EditIcon size={20} className="opacity-50" />
</button>
<button className="rounded bg-blue-500 p-2 text-white hover:bg-blue-600">
<EditIcon size={20} />
</button>
</div>
<div className="flex gap-4 text-xs text-gray-600">
<span>Normal</span>
<span>Disabled</span>
<span>Active</span>
</div>
</div>
)
}
// In Context
export const InContext: Story = {
render: () => (
<div className="space-y-4">
<div className="rounded-lg border border-gray-200 p-4">
<div className="flex items-center justify-between">
<h3 className="font-semibold">Document.pdf</h3>
<div className="flex gap-2">
<button className="rounded p-1 hover:bg-gray-100">
<CopyIcon size={16} />
</button>
<button className="rounded p-1 hover:bg-gray-100">
<EditIcon size={16} />
</button>
<button className="rounded p-1 hover:bg-gray-100">
<DeleteIcon size={16} />
</button>
</div>
</div>
</div>
<div className="rounded-lg border border-gray-200 p-4">
<div className="mb-3 flex items-center gap-2">
<VisionIcon size={20} />
<span className="font-medium">Image Processing</span>
</div>
<p className="mb-3 text-sm text-gray-600">Process your images with advanced AI tools</p>
<button className="flex items-center gap-2 rounded bg-blue-500 px-3 py-1 text-sm text-white hover:bg-blue-600">
<OcrIcon size={16} />
<span>Extract Text</span>
</button>
</div>
<div className="rounded-lg border border-gray-200 p-4">
<div className="flex items-center justify-between">
<span className="text-sm">Auto-refresh</span>
<RefreshIcon size={18} className="animate-spin text-blue-500" />
</div>
</div>
</div>
)
}
// Icon Grid
export const IconGrid: Story = {
render: () => {
const AllIcons = [
{ Icon: CopyIcon, name: 'Copy' },
{ Icon: DeleteIcon, name: 'Delete' },
{ Icon: EditIcon, name: 'Edit' },
{ Icon: RefreshIcon, name: 'Refresh' },
{ Icon: ResetIcon, name: 'Reset' },
{ Icon: ToolIcon, name: 'Tool' },
{ Icon: VisionIcon, name: 'Vision' },
{ Icon: WebSearchIcon, name: 'Search' },
{ Icon: WrapIcon, name: 'Wrap' },
{ Icon: UnWrapIcon, name: 'Unwrap' },
{ Icon: OcrIcon, name: 'OCR' }
]
return (
<div className="grid grid-cols-6 gap-4">
{AllIcons.map(({ Icon, name }) => (
<div
key={name}
className="flex flex-col items-center gap-2 rounded-lg border border-gray-200 p-4 hover:border-blue-500"
>
<Icon size={24} />
<span className="text-xs">{name}</span>
</div>
))}
</div>
)
}
}

View File

@ -1,7 +1,57 @@
import type { Meta, StoryObj } from '@storybook/react-vite'
import { action } from 'storybook/actions'
import CodeEditor, { getCmThemeByName, getCmThemeNames } from '../../../src/components/interactive/CodeEditor'
import CodeEditor, {
getCmThemeByName,
getCmThemeNames,
LanguageConfig
} from '../../../src/components/interactive/CodeEditor'
// 示例语言配置 - 为 Storybook 提供更丰富的语言支持演示
const exampleLanguageConfig: LanguageConfig = {
JavaScript: {
type: 'programming',
extensions: ['.js', '.mjs', '.cjs'],
aliases: ['js', 'node']
},
TypeScript: {
type: 'programming',
extensions: ['.ts'],
aliases: ['ts']
},
Python: {
type: 'programming',
extensions: ['.py'],
aliases: ['python3', 'py']
},
JSON: {
type: 'data',
extensions: ['.json']
},
Markdown: {
type: 'prose',
extensions: ['.md', '.markdown'],
aliases: ['md']
},
HTML: {
type: 'markup',
extensions: ['.html', '.htm']
},
CSS: {
type: 'markup',
extensions: ['.css']
},
'Graphviz (DOT)': {
type: 'data',
extensions: ['.dot', '.gv'],
aliases: ['dot', 'graphviz']
},
Mermaid: {
type: 'markup',
extensions: ['.mmd', '.mermaid'],
aliases: ['mmd']
}
}
const meta: Meta<typeof CodeEditor> = {
title: 'Interactive/CodeEditor',
@ -11,7 +61,7 @@ const meta: Meta<typeof CodeEditor> = {
argTypes: {
language: {
control: 'select',
options: ['typescript', 'javascript', 'json', 'markdown', 'python', 'dot', 'mmd']
options: ['typescript', 'javascript', 'json', 'markdown', 'python', 'dot', 'mmd', 'go', 'rust', 'php']
},
theme: {
control: 'select',
@ -23,7 +73,11 @@ const meta: Meta<typeof CodeEditor> = {
wrapped: { control: 'boolean' },
height: { control: 'text' },
maxHeight: { control: 'text' },
minHeight: { control: 'text' }
minHeight: { control: 'text' },
languageConfig: {
control: false,
description: 'Optional language configuration. If not provided, uses built-in defaults.'
}
}
}
@ -46,6 +100,7 @@ export const Default: Story = {
<CodeEditor
value={args.value as string}
language={args.language as string}
languageConfig={exampleLanguageConfig}
theme={getCmThemeByName((args as any).theme || 'light')}
fontSize={args.fontSize as number}
editable={args.editable as boolean}
@ -79,6 +134,7 @@ export const JSONLint: Story = {
options={{ lint: true }}
wrapped
onChange={action('change')}
languageConfig={exampleLanguageConfig}
/>
</div>
)
@ -97,6 +153,7 @@ export const SaveShortcut: Story = {
<CodeEditor
value={args.value as string}
language={args.language as string}
languageConfig={exampleLanguageConfig}
theme={getCmThemeByName((args as any).theme || 'light')}
options={{ keymap: true }}
onSave={action('save')}
@ -107,3 +164,32 @@ export const SaveShortcut: Story = {
</div>
)
}
// 使用默认语言配置(展示组件的独立性)
export const DefaultLanguageConfig: Story = {
args: {
language: 'javascript',
theme: 'light',
value: `// 这个示例使用内置的默认语言配置
function fibonacci(n) {
if (n <= 1) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
}
console.log(fibonacci(10));`,
wrapped: true
},
render: (args) => (
<div className="w-[720px] space-y-3">
<CodeEditor
value={args.value as string}
language={args.language as string}
// 注意:这里没有传入 languageConfig使用默认配置
theme={getCmThemeByName((args as any).theme || 'light')}
onChange={action('change')}
wrapped
/>
<p className="text-xs text-gray-500"> languageConfig使</p>
</div>
)
}

View File

@ -1,22 +1,23 @@
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"moduleResolution": "bundler",
"declaration": true,
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"allowSyntheticDefaultImports": true,
"jsx": "react-jsx",
"lib": ["DOM", "DOM.Iterable", "ES6"],
"baseUrl": ".",
"declaration": true,
"forceConsistentCasingInFileNames": true,
"incremental": true,
"isolatedModules": true,
"noFallthroughCasesInSwitch": true
"jsx": "react-jsx",
"lib": ["DOM", "DOM.Iterable", "ES6"],
"module": "ESNext",
"moduleResolution": "bundler",
"noFallthroughCasesInSwitch": true,
"outDir": "./dist",
"resolveJsonModule": true,
"rootDir": "./src",
"skipLibCheck": true,
"strict": true,
"target": "ES2020"
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist", "**/*.test.*", "**/__tests__/**"]
"exclude": ["node_modules", "dist", "**/*.test.*", "**/__tests__/**"],
"include": ["src/**/*"]
}

View File

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