diff --git a/packages/ui/MIGRATION_STATUS.md b/packages/ui/MIGRATION_STATUS.md index d880b849b1..57eb5b5329 100644 --- a/packages/ui/MIGRATION_STATUS.md +++ b/packages/ui/MIGRATION_STATUS.md @@ -49,9 +49,9 @@ function MyComponent() { ## 迁移概览 - **总组件数**: 236 -- **已迁移**: 26 +- **已迁移**: 36 - **已重构**: 0 -- **待迁移**: 210 +- **待迁移**: 200 ## 组件状态表 @@ -68,11 +68,12 @@ function MyComponent() { | | SuccessTag | ✅ | ❌ | 成功标签 | | | TextBadge | ✅ | ❌ | 文本徽标 | | | WarnTag | ✅ | ❌ | 警告标签 | -| | CustomCollapse | ❌ | ❌ | 自定义折叠面板 | +| | CustomCollapse | ✅ | ❌ | 自定义折叠面板 | | **display** | | | | 显示组件 | | | Ellipsis | ✅ | ❌ | 文本省略 | | | ExpandableText | ✅ | ❌ | 可展开文本 | | | ThinkingEffect | ✅ | ❌ | 思考效果动画 | +| | EmojiAvatar | ✅ | ❌ | 表情头像 | | | CodeViewer | ❌ | ❌ | 代码查看器 (外部依赖) | | | OGCard | ❌ | ❌ | OG 卡片 | | | MarkdownShadowDOMRenderer | ❌ | ❌ | Markdown 渲染器 | @@ -90,19 +91,24 @@ function MyComponent() { | | FileIcons | ✅ | ❌ | 文件图标 (包含 FileSvgIcon、FilePngIcon) | | | ReasoningIcon | ✅ | ❌ | 推理图标 | | | RefreshIcon | ✅ | ❌ | 刷新图标 | +| | ResetIcon | ✅ | ❌ | 重置图标 | | | SvgSpinners180Ring | ✅ | ❌ | 旋转加载图标 | | | ToolsCallingIcon | ✅ | ❌ | 工具调用图标 | | | VisionIcon | ✅ | ❌ | 视觉图标 | | | WebSearchIcon | ✅ | ❌ | 网页搜索图标 | +| | WrapIcon | ✅ | ❌ | 换行图标 | +| | UnWrapIcon | ✅ | ❌ | 不换行图标 | +| | OcrIcon | ✅ | ❌ | OCR 图标 | +| | ToolIcon | ✅ | ❌ | 工具图标 | | | Other icons | ❌ | ❌ | 其他图标文件 | | **interactive** | | | | 交互组件 | | | InfoTooltip | ✅ | ❌ | 信息提示 | -| | HelpTooltip | ❌ | ❌ | 帮助提示 | -| | WarnTooltip | ❌ | ❌ | 警告提示 | +| | HelpTooltip | ✅ | ❌ | 帮助提示 | +| | WarnTooltip | ✅ | ❌ | 警告提示 | | | DraggableList | ❌ | ❌ | 可拖拽列表 | | | EditableNumber | ❌ | ❌ | 可编辑数字 | | | EmojiPicker | ❌ | ❌ | 表情选择器 | -| | Selector | ❌ | ❌ | 选择器 | +| | Selector | ✅ | ❌ | 选择器 (i18n 依赖) | | | ModelSelector | ❌ | ❌ | 模型选择器 (Redux 依赖) | | | LanguageSelect | ❌ | ❌ | 语言选择 | | | TranslateButton | ❌ | ❌ | 翻译按钮 (window.api 依赖) | diff --git a/packages/ui/MIGRATION_STATUS_EN.md b/packages/ui/MIGRATION_STATUS_EN.md index 724f62f469..60c041204c 100644 --- a/packages/ui/MIGRATION_STATUS_EN.md +++ b/packages/ui/MIGRATION_STATUS_EN.md @@ -48,9 +48,9 @@ When submitting PRs, please place components in the correct directory based on t ## Migration Overview - **Total Components**: 236 -- **Migrated**: 26 +- **Migrated**: 36 - **Refactored**: 0 -- **Pending Migration**: 210 +- **Pending Migration**: 200 ## Component Status Table @@ -67,11 +67,12 @@ When submitting PRs, please place components in the correct directory based on t | | SuccessTag | ✅ | ❌ | Success tag | | | TextBadge | ✅ | ❌ | Text badge | | | WarnTag | ✅ | ❌ | Warning tag | -| | CustomCollapse | ❌ | ❌ | Custom collapse panel | +| | CustomCollapse | ✅ | ❌ | Custom collapse panel | | **display** | | | | Display components | | | Ellipsis | ✅ | ❌ | Text ellipsis | | | ExpandableText | ✅ | ❌ | Expandable text | | | ThinkingEffect | ✅ | ❌ | Thinking effect animation | +| | EmojiAvatar | ✅ | ❌ | Emoji avatar | | | CodeViewer | ❌ | ❌ | Code viewer (external deps) | | | OGCard | ❌ | ❌ | OG card | | | MarkdownShadowDOMRenderer | ❌ | ❌ | Markdown renderer | @@ -89,19 +90,24 @@ When submitting PRs, please place components in the correct directory based on t | | FileIcons | ✅ | ❌ | File icons (includes FileSvgIcon, FilePngIcon) | | | ReasoningIcon | ✅ | ❌ | Reasoning icon | | | RefreshIcon | ✅ | ❌ | Refresh icon | +| | ResetIcon | ✅ | ❌ | Reset icon | | | SvgSpinners180Ring | ✅ | ❌ | Spinners icon | | | ToolsCallingIcon | ✅ | ❌ | Tools calling icon | | | VisionIcon | ✅ | ❌ | Vision icon | | | WebSearchIcon | ✅ | ❌ | Web search icon | +| | WrapIcon | ✅ | ❌ | Wrap icon | +| | UnWrapIcon | ✅ | ❌ | Unwrap icon | +| | OcrIcon | ✅ | ❌ | OCR icon | +| | ToolIcon | ✅ | ❌ | Tool icon | | | Other icons | ❌ | ❌ | Other icon files | | **interactive** | | | | Interactive components | | | InfoTooltip | ✅ | ❌ | Info tooltip | -| | HelpTooltip | ❌ | ❌ | Help tooltip | -| | WarnTooltip | ❌ | ❌ | Warning tooltip | +| | HelpTooltip | ✅ | ❌ | Help tooltip | +| | WarnTooltip | ✅ | ❌ | Warning tooltip | | | DraggableList | ❌ | ❌ | Draggable list | | | EditableNumber | ❌ | ❌ | Editable number | | | EmojiPicker | ❌ | ❌ | Emoji picker | -| | Selector | ❌ | ❌ | Selector | +| | Selector | ✅ | ❌ | Selector (i18n dependency) | | | ModelSelector | ❌ | ❌ | Model selector (Redux dependency) | | | LanguageSelect | ❌ | ❌ | Language select | | | TranslateButton | ❌ | ❌ | Translate button (window.api dependency) | @@ -148,4 +154,4 @@ When submitting PRs, please place components in the correct directory based on t 3. **Submission Guidelines**: - Each PR should focus on one category of components - Ensure all migrated components are exported - - Update migration status in this document \ No newline at end of file + - Update migration status in this document diff --git a/packages/ui/src/components/base/CustomCollapse/index.tsx b/packages/ui/src/components/base/CustomCollapse/index.tsx new file mode 100644 index 0000000000..ef5cd8a51e --- /dev/null +++ b/packages/ui/src/components/base/CustomCollapse/index.tsx @@ -0,0 +1,109 @@ +// Original path: src/renderer/src/components/CustomCollapse.tsx +import { Collapse } from 'antd' +import { merge } from 'lodash' +import { ChevronRight } from 'lucide-react' +import { FC, memo, useMemo, useState } from 'react' + +interface CustomCollapseProps { + label: React.ReactNode + extra: React.ReactNode + children: React.ReactNode + destroyInactivePanel?: boolean + defaultActiveKey?: string[] + activeKey?: string[] + collapsible?: 'header' | 'icon' | 'disabled' + onChange?: (activeKeys: string | string[]) => void + style?: React.CSSProperties + styles?: { + header?: React.CSSProperties + body?: React.CSSProperties + } +} + +const CustomCollapse: FC = ({ + label, + extra, + children, + destroyInactivePanel = false, + defaultActiveKey = ['1'], + activeKey, + collapsible = undefined, + onChange, + style, + styles +}) => { + const [activeKeys, setActiveKeys] = useState(activeKey || defaultActiveKey) + + const defaultCollapseStyle = { + width: '100%', + background: 'transparent', + border: '0.5px solid var(--color-border)' + } + + const defaultCollpaseHeaderStyle = { + padding: '3px 16px', + alignItems: 'center', + justifyContent: 'space-between', + background: 'var(--color-background-soft)' + } + + const getHeaderStyle = () => { + return activeKeys && activeKeys.length > 0 + ? { + ...defaultCollpaseHeaderStyle, + borderTopLeftRadius: '8px', + borderTopRightRadius: '8px' + } + : { + ...defaultCollpaseHeaderStyle, + borderRadius: '8px' + } + } + + const defaultCollapseItemStyles = { + header: getHeaderStyle(), + body: { + borderTop: 'none' + } + } + + const collapseStyle = merge({}, defaultCollapseStyle, style) + const collapseItemStyles = useMemo(() => { + return merge({}, defaultCollapseItemStyles, styles) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [activeKeys]) + + return ( + { + setActiveKeys(keys) + onChange?.(keys) + }} + expandIcon={({ isActive }) => ( + + )} + items={[ + { + styles: collapseItemStyles, + key: '1', + label, + extra, + children + } + ]} + /> + ) +} + +export default memo(CustomCollapse) diff --git a/packages/ui/src/components/display/EmojiAvatar/index.tsx b/packages/ui/src/components/display/EmojiAvatar/index.tsx new file mode 100644 index 0000000000..2cc7bc925e --- /dev/null +++ b/packages/ui/src/components/display/EmojiAvatar/index.tsx @@ -0,0 +1,54 @@ +// Original path: src/renderer/src/components/Avatar/EmojiAvatar.tsx +import React, { memo } from 'react' +import styled from 'styled-components' + +interface EmojiAvatarProps { + children: string + size?: number + fontSize?: number + onClick?: React.MouseEventHandler + className?: string + style?: React.CSSProperties +} + +const EmojiAvatar = ({ + ref, + children, + size = 31, + fontSize, + onClick, + className, + style +}: EmojiAvatarProps & { ref?: React.RefObject }) => ( + + {children} + +) + +EmojiAvatar.displayName = 'EmojiAvatar' + +const StyledEmojiAvatar = styled.div<{ $size: number; $fontSize: number }>` + display: flex; + align-items: center; + justify-content: center; + background-color: var(--color-background-soft); + border: 0.5px solid var(--color-border); + border-radius: 20%; + cursor: pointer; + width: ${(props) => props.$size}px; + height: ${(props) => props.$size}px; + font-size: ${(props) => props.$fontSize}px; + transition: opacity 0.3s ease; + + &:hover { + opacity: 0.8; + } +` + +export default memo(EmojiAvatar) diff --git a/packages/ui/src/components/icons/OcrIcon/index.tsx b/packages/ui/src/components/icons/OcrIcon/index.tsx new file mode 100644 index 0000000000..68a317de33 --- /dev/null +++ b/packages/ui/src/components/icons/OcrIcon/index.tsx @@ -0,0 +1,8 @@ +// Original path: src/renderer/src/components/Icons/OcrIcon.tsx +import { FC } from 'react' + +const OcrIcon: FC, HTMLElement>> = (props) => { + return +} + +export default OcrIcon diff --git a/packages/ui/src/components/icons/ResetIcon/index.tsx b/packages/ui/src/components/icons/ResetIcon/index.tsx new file mode 100644 index 0000000000..364c8ddb91 --- /dev/null +++ b/packages/ui/src/components/icons/ResetIcon/index.tsx @@ -0,0 +1,6 @@ +// Original path: src/renderer/src/components/Icons/ResetIcon.tsx +import { RotateCcw } from 'lucide-react' + +const ResetIcon = (props: React.ComponentProps) => + +export default ResetIcon diff --git a/packages/ui/src/components/icons/ToolIcon/index.tsx b/packages/ui/src/components/icons/ToolIcon/index.tsx new file mode 100644 index 0000000000..d7486d5d22 --- /dev/null +++ b/packages/ui/src/components/icons/ToolIcon/index.tsx @@ -0,0 +1,8 @@ +// Original path: src/renderer/src/components/Icons/ToolIcon.tsx +import { FC } from 'react' + +const ToolIcon: FC, HTMLElement>> = (props) => { + return +} + +export default ToolIcon diff --git a/packages/ui/src/components/icons/UnWrapIcon/index.tsx b/packages/ui/src/components/icons/UnWrapIcon/index.tsx new file mode 100644 index 0000000000..7c9e814d38 --- /dev/null +++ b/packages/ui/src/components/icons/UnWrapIcon/index.tsx @@ -0,0 +1,18 @@ +// Original path: src/renderer/src/components/Icons/UnWrapIcon.tsx +const UnWrapIcon = (props: React.SVGProps) => ( + + + +) +export default UnWrapIcon diff --git a/packages/ui/src/components/icons/WrapIcon/index.tsx b/packages/ui/src/components/icons/WrapIcon/index.tsx new file mode 100644 index 0000000000..8de3b4a350 --- /dev/null +++ b/packages/ui/src/components/icons/WrapIcon/index.tsx @@ -0,0 +1,21 @@ +// Original path: src/renderer/src/components/Icons/WrapIcon.tsx +import React from 'react' + +const WrapIcon = (props: React.SVGProps) => ( + + + + +) +export default WrapIcon diff --git a/packages/ui/src/components/index.ts b/packages/ui/src/components/index.ts index 05d5757043..3e9133eea1 100644 --- a/packages/ui/src/components/index.ts +++ b/packages/ui/src/components/index.ts @@ -1,5 +1,6 @@ // Base Components export { default as CopyButton } from './base/CopyButton' +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' @@ -12,6 +13,7 @@ export { WarnTag } from './base/WarnTag' // Display Components export { default as Ellipsis } from './display/Ellipsis' +export { default as EmojiAvatar } from './display/EmojiAvatar' export { default as ExpandableText } from './display/ExpandableText' export { default as ThinkingEffect } from './display/ThinkingEffect' @@ -24,15 +26,23 @@ export { default as CopyIcon } from './icons/CopyIcon' export { default as DeleteIcon } from './icons/DeleteIcon' export { default as EditIcon } from './icons/EditIcon' 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 { default as HelpTooltip } from './interactive/HelpTooltip' export { default as InfoTooltip } from './interactive/InfoTooltip' +export { default as Selector } from './interactive/Selector' +export { default as WarnTooltip } from './interactive/WarnTooltip' // Composite Components (复合组件) // 暂无复合组件 diff --git a/packages/ui/src/components/interactive/HelpTooltip/index.tsx b/packages/ui/src/components/interactive/HelpTooltip/index.tsx new file mode 100644 index 0000000000..8a6b9bd64c --- /dev/null +++ b/packages/ui/src/components/interactive/HelpTooltip/index.tsx @@ -0,0 +1,21 @@ +// Original path: src/renderer/src/components/TooltipIcons/HelpTooltip.tsx +import { Tooltip, TooltipProps } from 'antd' +import { HelpCircle } from 'lucide-react' + +type InheritedTooltipProps = Omit + +interface HelpTooltipProps extends InheritedTooltipProps { + iconColor?: string + iconSize?: string | number + iconStyle?: React.CSSProperties +} + +const HelpTooltip = ({ iconColor = 'var(--color-text-2)', iconSize = 14, iconStyle, ...rest }: HelpTooltipProps) => { + return ( + + + + ) +} + +export default HelpTooltip diff --git a/packages/ui/src/components/interactive/Selector/index.tsx b/packages/ui/src/components/interactive/Selector/index.tsx new file mode 100644 index 0000000000..03710a4246 --- /dev/null +++ b/packages/ui/src/components/interactive/Selector/index.tsx @@ -0,0 +1,197 @@ +// Original path: src/renderer/src/components/Selector.tsx +import { Dropdown, DropdownProps } from 'antd' +import { Check, ChevronsUpDown } from 'lucide-react' +import { ReactNode, useEffect, useMemo, useRef, useState } from 'react' +import { useTranslation } from 'react-i18next' +import styled, { css } from 'styled-components' + +interface SelectorOption { + label: string | ReactNode + value: V + type?: 'group' + options?: SelectorOption[] + disabled?: boolean +} + +interface BaseSelectorProps { + options: SelectorOption[] + placeholder?: string + placement?: 'topLeft' | 'topCenter' | 'topRight' | 'bottomLeft' | 'bottomCenter' | 'bottomRight' | 'top' | 'bottom' + /** 字体大小 */ + size?: number + /** 是否禁用 */ + disabled?: boolean +} + +interface SingleSelectorProps extends BaseSelectorProps { + multiple?: false + value?: V + onChange: (value: V) => void +} + +interface MultipleSelectorProps extends BaseSelectorProps { + multiple: true + value?: V[] + onChange: (value: V[]) => void +} + +type SelectorProps = SingleSelectorProps | MultipleSelectorProps + +const Selector = ({ + options, + value, + onChange = () => {}, + placement = 'bottomRight', + size = 13, + placeholder, + disabled = false, + multiple = false +}: SelectorProps) => { + const [open, setOpen] = useState(false) + const { t } = useTranslation() + const inputRef = useRef(null) + + useEffect(() => { + let timer: NodeJS.Timeout + if (open) { + timer = setTimeout(() => { + inputRef.current?.focus() + }, 1) + } + return () => { + clearTimeout(timer) + } + }, [open]) + + const selectedValues = useMemo(() => { + if (multiple) { + return (value as V[]) || [] + } + return value !== undefined ? [value as V] : [] + }, [value, multiple]) + + const label = useMemo(() => { + if (selectedValues.length > 0) { + const findLabels = (opts: SelectorOption[]): (string | ReactNode)[] => { + const labels: (string | ReactNode)[] = [] + for (const opt of opts) { + if (selectedValues.some((v) => v == opt.value)) { + labels.push(opt.label) + } + if (opt.options) { + labels.push(...findLabels(opt.options)) + } + } + return labels + } + const labels = findLabels(options) + if (labels.length === 0) return placeholder + if (labels.length === 1) return labels[0] + return t('common.selectedItems', { count: labels.length }) + } + return placeholder + }, [selectedValues, placeholder, options, t]) + + const items = useMemo(() => { + const mapOption = (option: SelectorOption) => ({ + key: option.value, + label: option.label, + extra: {selectedValues.some((v) => v == option.value) && }, + disabled: option.disabled, + type: option.type || (option.options ? 'group' : undefined), + children: option.options?.map(mapOption) + }) + + return options.map(mapOption) + }, [options, selectedValues]) + + function onClick(e: { key: string }) { + if (disabled) return + + const newValue = e.key as V + if (multiple) { + const newValues = selectedValues.includes(newValue) + ? selectedValues.filter((v) => v !== newValue) + : [...selectedValues, newValue] + ;(onChange as MultipleSelectorProps['onChange'])(newValues) + } else { + ;(onChange as SingleSelectorProps['onChange'])(newValue) + setOpen(false) + } + } + + const handleOpenChange: DropdownProps['onOpenChange'] = (nextOpen, info) => { + if (disabled) return + + if (info.source === 'trigger' || nextOpen) { + setOpen(nextOpen) + } + } + + return ( + + + + ) +} + +const LabelIcon = styled(ChevronsUpDown)` + border-radius: 4px; + padding: 2px 0; + background-color: var(--color-background-soft); + transition: background-color 0.2s; +` + +const Label = styled.div<{ $size: number; $open: boolean; $disabled: boolean; $isPlaceholder: boolean }>` + display: flex; + align-items: center; + gap: 4px; + border-radius: 99px; + padding: 3px 2px 3px 10px; + font-size: ${({ $size }) => $size}px; + line-height: 1; + cursor: ${({ $disabled }) => ($disabled ? 'not-allowed' : 'pointer')}; + opacity: ${({ $disabled }) => ($disabled ? 0.6 : 1)}; + color: ${({ $isPlaceholder }) => ($isPlaceholder ? 'var(--color-text-2)' : 'inherit')}; + + transition: + background-color 0.2s, + opacity 0.2s; + &:hover { + ${({ $disabled }) => + !$disabled && + css` + background-color: var(--color-background-mute); + ${LabelIcon} { + background-color: var(--color-background-mute); + } + `} + } + ${({ $open, $disabled }) => + $open && + !$disabled && + css` + background-color: var(--color-background-mute); + ${LabelIcon} { + background-color: var(--color-background-mute); + } + `} +` + +const CheckIcon = styled.div` + width: 20px; + display: flex; + align-items: center; + justify-content: end; +` + +export default Selector diff --git a/packages/ui/src/components/interactive/WarnTooltip/index.tsx b/packages/ui/src/components/interactive/WarnTooltip/index.tsx new file mode 100644 index 0000000000..96397a82ea --- /dev/null +++ b/packages/ui/src/components/interactive/WarnTooltip/index.tsx @@ -0,0 +1,26 @@ +// Original path: src/renderer/src/components/TooltipIcons/WarnTooltip.tsx +import { Tooltip, TooltipProps } from 'antd' +import { AlertTriangle } from 'lucide-react' + +type InheritedTooltipProps = Omit + +interface WarnTooltipProps extends InheritedTooltipProps { + iconColor?: string + iconSize?: string | number + iconStyle?: React.CSSProperties +} + +const WarnTooltip = ({ + iconColor = 'var(--color-status-warning)', + iconSize = 14, + iconStyle, + ...rest +}: WarnTooltipProps) => { + return ( + + + + ) +} + +export default WarnTooltip