feat: enhance Selector component with SearchableSelector and update exports

- Introduced a new SearchableSelector component for improved item selection with search functionality.
- Updated the Selector component to streamline item selection and added type exports for better type safety.
- Refactored the preferenceSchemas to use the new MathEngine type for better clarity.
- Added comprehensive README documentation for the Selector component detailing usage and features.
- Updated various components and stories to utilize the new Selector and SearchableSelector components.
This commit is contained in:
MyPrototypeWhat 2025-09-30 14:59:33 +08:00
parent 6c71b92d1d
commit db4fcac768
10 changed files with 711 additions and 139 deletions

View File

@ -130,7 +130,7 @@ export interface PreferenceSchemas {
// redux/settings/fontSize
'chat.message.font_size': number
// redux/settings/mathEngine
'chat.message.math.engine': string
'chat.message.math.engine': PreferenceTypes.MathEngine
// redux/settings/mathEnableSingleDollar
'chat.message.math.single_dollar': boolean
// redux/settings/foldDisplayMode

View File

@ -70,6 +70,8 @@ export type ProxyMode = 'system' | 'custom' | 'none'
export type MultiModelFoldDisplayMode = 'expanded' | 'compact'
export type MathEngine = 'KaTeX' | 'MathJax' | 'none'
export enum UpgradeChannel {
LATEST = 'latest', // 最新稳定版本
RC = 'rc', // 公测版本

View File

@ -0,0 +1,333 @@
# Selector 组件
基于 HeroUI Select 封装的下拉选择组件,简化了 Set 和 Selection 的转换逻辑。
## 核心特性
- ✅ **类型安全**: 单选和多选自动推断回调类型
- ✅ **智能转换**: 自动处理 `Set<Key>` 和原始值的转换
- ✅ **HeroUI 风格**: 保持与 HeroUI 生态一致的 API
- ✅ **支持数字和字符串**: 泛型支持,自动识别值类型
## 基础用法
### 单选模式(默认)
```tsx
import { Selector } from '@cherrystudio/ui'
import { useState } from 'react'
function Example() {
const [language, setLanguage] = useState('zh-CN')
const languageOptions = [
{ label: '中文', value: 'zh-CN' },
{ label: 'English', value: 'en-US' },
{ label: '日本語', value: 'ja-JP' }
]
return (
<Selector
selectedKeys={language}
onSelectionChange={(value) => {
// value 类型自动推断为 string
setLanguage(value)
}}
items={languageOptions}
placeholder="选择语言"
/>
)
}
```
### 多选模式
```tsx
import { Selector } from '@cherrystudio/ui'
import { useState } from 'react'
function Example() {
const [languages, setLanguages] = useState(['zh-CN', 'en-US'])
const languageOptions = [
{ label: '中文', value: 'zh-CN' },
{ label: 'English', value: 'en-US' },
{ label: '日本語', value: 'ja-JP' },
{ label: 'Français', value: 'fr-FR' }
]
return (
<Selector
selectionMode="multiple"
selectedKeys={languages}
onSelectionChange={(values) => {
// values 类型自动推断为 string[]
setLanguages(values)
}}
items={languageOptions}
placeholder="选择语言"
/>
)
}
```
### 数字类型值
```tsx
import { Selector } from '@cherrystudio/ui'
function Example() {
const [priority, setPriority] = useState<number>(1)
const priorityOptions = [
{ label: '低', value: 1 },
{ label: '中', value: 2 },
{ label: '高', value: 3 }
]
return (
<Selector<number>
selectedKeys={priority}
onSelectionChange={(value) => {
// value 类型为 number
setPriority(value)
}}
items={priorityOptions}
/>
)
}
```
### 禁用选项
```tsx
const options = [
{ label: '选项 1', value: '1' },
{ label: '选项 2 (禁用)', value: '2', disabled: true },
{ label: '选项 3', value: '3' }
]
<Selector
selectedKeys="1"
onSelectionChange={handleChange}
items={options}
/>
```
### 自定义 Label
```tsx
import { Flex } from '@cherrystudio/ui'
const options = [
{
label: (
<Flex className="items-center gap-2">
<span>🇨🇳</span>
<span>中文</span>
</Flex>
),
value: 'zh-CN'
},
{
label: (
<Flex className="items-center gap-2">
<span>🇺🇸</span>
<span>English</span>
</Flex>
),
value: 'en-US'
}
]
<Selector
selectedKeys="zh-CN"
onSelectionChange={handleChange}
items={options}
/>
```
## API
### SelectorProps
| 属性 | 类型 | 默认值 | 说明 |
|------|------|--------|------|
| `items` | `SelectorItem<V>[]` | - | 必填,选项列表 |
| `selectedKeys` | `V` \| `V[]` | - | 受控的选中值(单选为单个值,多选为数组) |
| `onSelectionChange` | `(key: V) => void` \| `(keys: V[]) => void` | - | 选择变化回调(类型根据 selectionMode 自动推断) |
| `selectionMode` | `'single'` \| `'multiple'` | `'single'` | 选择模式 |
| `placeholder` | `string` | - | 占位文本 |
| `disabled` | `boolean` | `false` | 是否禁用 |
| `isRequired` | `boolean` | `false` | 是否必填 |
| `label` | `ReactNode` | - | 标签文本 |
| `description` | `ReactNode` | - | 描述文本 |
| `errorMessage` | `ReactNode` | - | 错误提示 |
| ...rest | `SelectProps` | - | 其他 HeroUI Select 属性 |
### SelectorItem
```tsx
interface SelectorItem<V = string | number> {
label: string | ReactNode // 显示文本或自定义内容
value: V // 选项值
disabled?: boolean // 是否禁用
[key: string]: any // 其他自定义属性
}
```
## 类型安全
组件使用 TypeScript 条件类型,根据 `selectionMode` 自动推断回调类型:
```tsx
// 单选模式
<Selector
selectionMode="single" // 或省略(默认单选)
selectedKeys={value} // 类型: V
onSelectionChange={(v) => ...} // v 类型: V
/>
// 多选模式
<Selector
selectionMode="multiple"
selectedKeys={values} // 类型: V[]
onSelectionChange={(vs) => ...} // vs 类型: V[]
/>
```
## 与 HeroUI Select 的区别
| 特性 | HeroUI Select | Selector (本组件) |
|------|---------------|------------------|
| `selectedKeys` | `Set<Key> \| 'all'` | `V` \| `V[]` (自动转换) |
| `onSelectionChange` | `(keys: Selection) => void` | `(key: V) => void` \| `(keys: V[]) => void` |
| 单选回调 | 返回 `Set` (需手动提取) | 直接返回单个值 |
| 多选回调 | 返回 `Set` (需转数组) | 直接返回数组 |
| 类型推断 | 无 | 根据 selectionMode 自动推断 |
## 最佳实践
### 1. 显式声明 selectionMode
虽然单选是默认模式,但建议显式声明以提高代码可读性:
```tsx
// ✅ 推荐
<Selector selectionMode="single" ... />
// ⚠️ 可以但不够清晰
<Selector ... />
```
### 2. 使用泛型指定值类型
当值类型为数字或联合类型时,使用泛型获得更好的类型提示:
```tsx
// ✅ 推荐
<Selector<number> selectedKeys={priority} ... />
// ✅ 推荐(联合类型)
type Status = 'pending' | 'approved' | 'rejected'
<Selector<Status> selectedKeys={status} ... />
```
### 3. 避免在渲染时创建 items
```tsx
// ❌ 不推荐(每次渲染都创建新数组)
<Selector items={[{ label: 'A', value: '1' }]} />
// ✅ 推荐(在组件外或使用 useMemo
const items = [{ label: 'A', value: '1' }]
<Selector items={items} />
```
## 迁移指南
### 从 antd Select 迁移
```tsx
// antd Select
import { Select } from 'antd'
<Select
value={value}
onChange={(value) => onChange(value)}
options={[
{ label: 'A', value: '1' },
{ label: 'B', value: '2' }
]}
/>
// 迁移到 Selector
import { Selector } from '@cherrystudio/ui'
<Selector
selectedKeys={value} // value → selectedKeys
onSelectionChange={(value) => onChange(value)} // onChange → onSelectionChange
items={[ // options → items
{ label: 'A', value: '1' },
{ label: 'B', value: '2' }
]}
/>
```
### 从旧版 Selector 迁移
```tsx
// 旧版 Selector (返回数组)
<Selector
onSelectionChange={(values) => {
const value = values[0] // 需要手动提取
onChange(value)
}}
/>
// 新版 Selector (直接返回值)
<Selector
selectionMode="single"
onSelectionChange={(value) => {
onChange(value) // 直接使用
}}
/>
```
## 常见问题
### Q: 为什么单选模式下还需要 selectedKeys 而不是 selectedKey
A: 为了保持与 HeroUI API 命名的一致性,同时简化组件实现。组件内部会自动处理单个值和 Set 的转换。
### Q: 如何清空选择?
```tsx
// 单选模式
<Selector
selectedKeys={value}
onSelectionChange={setValue}
isClearable // 添加清空按钮
/>
// 或手动设置为 undefined
setValue(undefined)
```
### Q: 支持异步加载选项吗?
支持,配合 `isLoading` 属性使用:
```tsx
const [items, setItems] = useState([])
const [loading, setLoading] = useState(true)
useEffect(() => {
fetchItems().then(data => {
setItems(data)
setLoading(false)
})
}, [])
<Selector items={items} isLoading={loading} />
```

View File

@ -0,0 +1,60 @@
import { Autocomplete, AutocompleteItem } from '@heroui/react'
import type { Key } from '@react-types/shared'
import { useMemo } from 'react'
import type { SearchableSelectorItem, SearchableSelectorProps } from './types'
const SearchableSelector = <T extends SearchableSelectorItem>(props: SearchableSelectorProps<T>) => {
const { items, onSelectionChange, selectedKeys, selectionMode = 'single', children, ...rest } = props
// 转换 selectedKeys: V | V[] → Key | undefined (Autocomplete 只支持单选)
const autocompleteSelectedKey = useMemo(() => {
if (selectedKeys === undefined) return undefined
if (selectionMode === 'multiple') {
// Autocomplete 不支持多选,取第一个
const keys = selectedKeys as T['value'][]
return keys.length > 0 ? String(keys[0]) : undefined
} else {
return String(selectedKeys)
}
}, [selectedKeys, selectionMode])
// 处理选择变化
const handleSelectionChange = (key: Key | null) => {
if (!onSelectionChange || key === null) return
const strKey = String(key)
// 尝试转换回数字类型
const num = Number(strKey)
const value = !isNaN(num) && items.some((item) => item.value === num) ? (num as T['value']) : (strKey as T['value'])
if (selectionMode === 'multiple') {
// 多选模式: 返回数组 (Autocomplete 只支持单选,这里简化处理)
;(onSelectionChange as (keys: T['value'][]) => void)([value])
} else {
// 单选模式: 返回单个值
;(onSelectionChange as (key: T['value']) => void)(value)
}
}
// 默认渲染函数
const defaultRenderItem = (item: T) => (
<AutocompleteItem key={String(item.value)} textValue={item.label ? String(item.label) : String(item.value)}>
{item.label ?? item.value}
</AutocompleteItem>
)
return (
<Autocomplete
{...rest}
items={items}
selectedKey={autocompleteSelectedKey}
onSelectionChange={handleSelectionChange}
allowsCustomValue={false}>
{children ?? defaultRenderItem}
</Autocomplete>
)
}
export default SearchableSelector

View File

@ -0,0 +1,75 @@
import type { Selection } from '@heroui/react'
import { Select, SelectItem } from '@heroui/react'
import type { Key } from '@react-types/shared'
import { useMemo } from 'react'
import type { SelectorItem, SelectorProps } from './types'
const Selector = <T extends SelectorItem>(props: SelectorProps<T>) => {
const { items, onSelectionChange, selectedKeys, selectionMode = 'single', children, ...rest } = props
// 转换 selectedKeys: V | V[] | undefined → Set<Key> | undefined
const heroUISelectedKeys = useMemo(() => {
if (selectedKeys === undefined) return undefined
if (selectionMode === 'multiple') {
// 多选模式: V[] → Set<Key>
return new Set((selectedKeys as T['value'][]).map((key) => String(key) as Key))
} else {
// 单选模式: V → Set<Key>
return new Set([String(selectedKeys) as Key])
}
}, [selectedKeys, selectionMode])
// 处理选择变化,转换 Selection → V | V[]
const handleSelectionChange = (keys: Selection) => {
if (!onSelectionChange) return
if (keys === 'all') {
// 如果是全选,返回所有非禁用项的值
const allValues = items.filter((item) => !item.disabled).map((item) => item.value)
if (selectionMode === 'multiple') {
;(onSelectionChange as (keys: T['value'][]) => void)(allValues)
}
return
}
// 转换 Set<Key> 为原始类型
const keysArray = Array.from(keys).map((key) => {
const strKey = String(key)
// 尝试转换回数字类型(如果原始值是数字)
const num = Number(strKey)
return !isNaN(num) && items.some((item) => item.value === num) ? (num as T['value']) : (strKey as T['value'])
})
if (selectionMode === 'multiple') {
// 多选模式: 返回数组
;(onSelectionChange as (keys: T['value'][]) => void)(keysArray)
} else {
// 单选模式: 返回单个值
if (keysArray.length > 0) {
;(onSelectionChange as (key: T['value']) => void)(keysArray[0])
}
}
}
// 默认渲染函数
const defaultRenderItem = (item: T) => (
<SelectItem key={String(item.value)} textValue={item.label ? String(item.label) : String(item.value)}>
{item.label ?? item.value}
</SelectItem>
)
return (
<Select
{...rest}
items={items}
selectionMode={selectionMode}
selectedKeys={heroUISelectedKeys as 'all' | Iterable<Key> | undefined}
onSelectionChange={handleSelectionChange}>
{children ?? defaultRenderItem}
</Select>
)
}
export default Selector

View File

@ -1,51 +1,13 @@
import type { Selection, SelectProps } from '@heroui/react'
import { Select, SelectItem } from '@heroui/react'
import type { ReactNode } from 'react'
interface SelectorItem<V = string | number> {
label: string | ReactNode
value: V
disabled?: boolean
[key: string]: any
}
interface SelectorProps<V = string | number> extends Omit<SelectProps, 'children' | 'onSelectionChange'> {
items: SelectorItem<V>[]
onSelectionChange?: (keys: V[]) => void
}
const Selector = <V extends string | number>({ items, onSelectionChange, ...rest }: SelectorProps<V>) => {
// 处理选择变化,转换 Set 为数组
const handleSelectionChange = (keys: Selection) => {
if (!onSelectionChange) return
if (keys === 'all') {
// 如果是全选,返回所有非禁用项的值
const allValues = items.filter((item) => !item.disabled).map((item) => item.value)
onSelectionChange(allValues)
return
}
// 转换 Set<Key> 为原始类型数组
const keysArray = Array.from(keys).map((key) => {
const strKey = String(key)
// 尝试转换回数字类型(如果原始值是数字)
const num = Number(strKey)
return !isNaN(num) && items.some((item) => item.value === num) ? num : strKey
}) as V[]
onSelectionChange(keysArray)
}
return (
<Select {...rest} items={items} onSelectionChange={handleSelectionChange}>
{({ value, label, ...restItem }: SelectorItem<V>) => (
<SelectItem {...restItem} key={value} title={String(label)}>
{label}
</SelectItem>
)}
</Select>
)
}
export default Selector
export type { SelectorItem, SelectorProps }
// 统一导出 Selector 相关组件和类型
export { default as SearchableSelector } from './SearchableSelector'
export { default } from './Selector'
export type {
MultipleSearchableSelectorProps,
MultipleSelectorProps,
SearchableSelectorItem,
SearchableSelectorProps,
SelectorItem,
SelectorProps,
SingleSearchableSelectorProps,
SingleSelectorProps
} from './types'

View File

@ -0,0 +1,79 @@
import type { AutocompleteProps, SelectProps } from '@heroui/react'
import type { ReactElement, ReactNode } from 'react'
interface SelectorItem<V = string | number> {
label?: string | ReactNode
value: V
disabled?: boolean
[key: string]: any
}
// 自定义渲染函数类型
type SelectorRenderItem<T> = (item: T) => ReactElement
// 单选模式的 Props
interface SingleSelectorProps<T extends SelectorItem = SelectorItem>
extends Omit<SelectProps<T>, 'children' | 'onSelectionChange' | 'selectedKeys' | 'selectionMode'> {
items: T[]
selectionMode?: 'single'
selectedKeys?: T['value']
onSelectionChange?: (key: T['value']) => void
children?: SelectorRenderItem<T>
}
// 多选模式的 Props
interface MultipleSelectorProps<T extends SelectorItem = SelectorItem>
extends Omit<SelectProps<T>, 'children' | 'onSelectionChange' | 'selectedKeys' | 'selectionMode'> {
items: T[]
selectionMode: 'multiple'
selectedKeys?: T['value'][]
onSelectionChange?: (keys: T['value'][]) => void
children?: SelectorRenderItem<T>
}
type SelectorProps<T extends SelectorItem = SelectorItem> = SingleSelectorProps<T> | MultipleSelectorProps<T>
interface SearchableSelectorItem<V = string | number> {
label?: string | ReactNode
value: V
disabled?: boolean
[key: string]: any
}
// 自定义渲染函数类型
type SearchableRenderItem<T> = (item: T) => ReactElement
// 单选模式的 Props
interface SingleSearchableSelectorProps<T extends SearchableSelectorItem = SearchableSelectorItem>
extends Omit<AutocompleteProps<T>, 'children' | 'onSelectionChange' | 'selectedKey' | 'selectionMode'> {
items: T[]
selectionMode?: 'single'
selectedKeys?: T['value']
onSelectionChange?: (key: T['value']) => void
children?: SearchableRenderItem<T>
}
// 多选模式的 Props
interface MultipleSearchableSelectorProps<T extends SearchableSelectorItem = SearchableSelectorItem>
extends Omit<AutocompleteProps<T>, 'children' | 'onSelectionChange' | 'selectedKey' | 'selectionMode'> {
items: T[]
selectionMode: 'multiple'
selectedKeys?: T['value'][]
onSelectionChange?: (keys: T['value'][]) => void
children?: SearchableRenderItem<T>
}
type SearchableSelectorProps<T extends SearchableSelectorItem = SearchableSelectorItem> =
| SingleSearchableSelectorProps<T>
| MultipleSearchableSelectorProps<T>
export type {
MultipleSearchableSelectorProps,
MultipleSelectorProps,
SearchableSelectorItem,
SearchableSelectorProps,
SelectorItem,
SelectorProps,
SingleSearchableSelectorProps,
SingleSelectorProps
}

View File

@ -1,5 +1,5 @@
// Base Components
export { Avatar, AvatarGroup, type AvatarProps,EmojiAvatar } from './base/Avatar'
export { Avatar, AvatarGroup, type AvatarProps, EmojiAvatar } from './base/Avatar'
export { default as Button, type ButtonProps } from './base/Button'
export { default as CopyButton } from './base/CopyButton'
export { default as CustomCollapse } from './base/CustomCollapse'
@ -48,8 +48,22 @@ export {
export { default as SvgSpinners180Ring } from './icons/SvgSpinners180Ring'
export { default as ToolsCallingIcon } from './icons/ToolsCallingIcon'
// Interactive Components
/* Interactive Components */
// Selector / SearchableSelector
export { default as Selector } from './base/Selector'
export { default as SearchableSelector } from './base/Selector/SearchableSelector'
export type {
MultipleSearchableSelectorProps,
MultipleSelectorProps,
SearchableSelectorItem,
SearchableSelectorProps,
SelectorItem,
SelectorProps,
SingleSearchableSelectorProps,
SingleSelectorProps
} from './base/Selector/types'
// CodeEditor
export {
default as CodeEditor,
type CodeEditorHandles,
@ -58,14 +72,22 @@ export {
getCmThemeByName,
getCmThemeNames
} from './interactive/CodeEditor'
// CollapsibleSearchBar
export { default as CollapsibleSearchBar } from './interactive/CollapsibleSearchBar'
// DraggableList
export { DraggableList, useDraggableReorder } from './interactive/DraggableList'
// EditableNumber
export type { EditableNumberProps } from './interactive/EditableNumber'
// EditableNumber
export { default as EditableNumber } from './interactive/EditableNumber'
export { default as HelpTooltip } from './interactive/HelpTooltip'
// ImageToolButton
export { default as ImageToolButton } from './interactive/ImageToolButton'
// InfoTooltip
export { default as InfoTooltip } from './interactive/InfoTooltip'
// Sortable
export { Sortable } from './interactive/Sortable'
// WarnTooltip
export { default as WarnTooltip } from './interactive/WarnTooltip'
// Composite Components (复合组件)

View File

@ -1,4 +1,4 @@
import type { Meta, StoryObj } from '@storybook/react'
import type { Meta } from '@storybook/react'
import { useState } from 'react'
import Selector from '../../../src/components/base/Selector'
@ -49,18 +49,18 @@ const meta: Meta<typeof Selector> = {
}
export default meta
type Story = StoryObj<typeof meta>
// 基础用法
export const Default: Story = {
// 基础用法 - 单选
export const Default = {
render: function Render() {
const [selectedKeys, setSelectedKeys] = useState<Set<string>>(new Set(['react']))
const [selectedValue, setSelectedValue] = useState<string>('react')
return (
<div className="space-y-4">
<Selector
selectedKeys={selectedKeys}
onSelectionChange={(keys) => setSelectedKeys(new Set(keys.map(String)))}
selectionMode="single"
selectedKeys={selectedValue}
onSelectionChange={(value) => setSelectedValue(value)}
placeholder="选择框架"
items={[
{ value: 'react', label: 'React' },
@ -70,7 +70,7 @@ export const Default: Story = {
]}
/>
<div className="text-sm text-gray-600">
: <code>{Array.from(selectedKeys).join(', ')}</code>
: <code>{selectedValue}</code>
</div>
</div>
)
@ -78,16 +78,16 @@ export const Default: Story = {
}
// 多选模式
export const Multiple: Story = {
export const Multiple = {
render: function Render() {
const [selectedKeys, setSelectedKeys] = useState<Set<string>>(new Set(['react', 'vue']))
const [selectedValues, setSelectedValues] = useState<string[]>(['react', 'vue'])
return (
<div className="space-y-4">
<Selector
selectionMode="multiple"
selectedKeys={selectedKeys}
onSelectionChange={(keys) => setSelectedKeys(new Set(keys.map(String)))}
selectedKeys={selectedValues}
onSelectionChange={(values) => setSelectedValues(values)}
placeholder="选择多个框架"
items={[
{ value: 'react', label: 'React' },
@ -98,7 +98,7 @@ export const Multiple: Story = {
]}
/>
<div className="text-sm text-gray-600">
({selectedKeys.size}): {Array.from(selectedKeys).join(', ')}
({selectedValues.length}): {selectedValues.join(', ')}
</div>
</div>
)
@ -106,19 +106,16 @@ export const Multiple: Story = {
}
// 数字值类型
export const NumberValues: Story = {
export const NumberValues = {
render: function Render() {
const [selectedKeys, setSelectedKeys] = useState<Set<string>>(new Set(['2']))
const [selectedValue, setSelectedValue] = useState<number>(2)
return (
<div className="space-y-4">
<Selector
selectedKeys={selectedKeys}
onSelectionChange={(keys) => {
setSelectedKeys(new Set(keys.map(String)))
setSelectedValue(keys[0] as number)
}}
selectionMode="single"
selectedKeys={selectedValue}
onSelectionChange={(value) => setSelectedValue(value)}
placeholder="选择优先级"
items={[
{ value: 1, label: '🔴 紧急' },
@ -136,7 +133,7 @@ export const NumberValues: Story = {
}
// 不同大小
export const Sizes: Story = {
export const Sizes = {
render: function Render() {
const items = [
{ value: 'option1', label: '选项 1' },
@ -164,22 +161,26 @@ export const Sizes: Story = {
}
// 禁用状态
export const Disabled: Story = {
args: {
isDisabled: true,
selectedKeys: new Set(['react']),
placeholder: '禁用的选择器',
items: [
{ value: 'react', label: 'React' },
{ value: 'vue', label: 'Vue' }
]
export const Disabled = {
render: function Render() {
return (
<Selector
isDisabled
selectedKeys="react"
placeholder="禁用的选择器"
items={[
{ value: 'react', label: 'React' },
{ value: 'vue', label: 'Vue' }
]}
/>
)
}
}
// 实际应用场景:语言选择
export const LanguageSelector: Story = {
export const LanguageSelector = {
render: function Render() {
const [selectedKeys, setSelectedKeys] = useState<Set<string>>(new Set(['zh']))
const [selectedValue, setSelectedValue] = useState<string>('zh')
const languages = [
{ value: 'zh', label: '🇨🇳 简体中文' },
@ -193,13 +194,14 @@ export const LanguageSelector: Story = {
return (
<div className="space-y-4">
<Selector
selectedKeys={selectedKeys}
onSelectionChange={(keys) => setSelectedKeys(new Set(keys.map(String)))}
selectionMode="single"
selectedKeys={selectedValue}
onSelectionChange={(value) => setSelectedValue(value)}
placeholder="选择语言"
items={languages}
/>
<div className="p-4 bg-gray-100 dark:bg-gray-800 rounded">
: <strong>{languages.find((l) => l.value === Array.from(selectedKeys)[0])?.label}</strong>
: <strong>{languages.find((l) => l.value === selectedValue)?.label}</strong>
</div>
</div>
)

View File

@ -1,4 +1,4 @@
import { Button, DescriptionSwitch, RowFlex, Selector } from '@cherrystudio/ui'
import { Button, DescriptionSwitch, RowFlex, Selector, type SelectorItem } from '@cherrystudio/ui'
import { useMultiplePreferences, usePreference } from '@data/hooks/usePreference'
import EditableNumber from '@renderer/components/EditableNumber'
import Scrollbar from '@renderer/components/Scrollbar'
@ -18,7 +18,7 @@ import { getDefaultModel } from '@renderer/services/AssistantService'
import type { Assistant, AssistantSettings, CodeStyleVarious, MathEngine } from '@renderer/types'
import { modalConfirm } from '@renderer/utils'
import { getSendMessageShortcutLabel } from '@renderer/utils/input'
import type { SendMessageShortcut } from '@shared/data/preference/preferenceTypes'
import type { MultiModelMessageStyle, SendMessageShortcut } from '@shared/data/preference/preferenceTypes'
import { ThemeMode } from '@shared/data/preference/preferenceTypes'
import { Col, InputNumber, Row, Slider } from 'antd'
import { Settings2 } from 'lucide-react'
@ -101,6 +101,63 @@ const SettingsTab: FC<Props> = (props) => {
const { t } = useTranslation()
const messageStyleItems = useMemo<SelectorItem<'plain' | 'bubble'>[]>(
() => [
{ value: 'plain', label: t('message.message.style.plain') },
{ value: 'bubble', label: t('message.message.style.bubble') }
],
[t]
)
const multiModelMessageStyleItems = useMemo<SelectorItem<MultiModelMessageStyle>[]>(
() => [
{ value: 'fold', label: t('message.message.multi_model_style.fold.label') },
{ value: 'vertical', label: t('message.message.multi_model_style.vertical') },
{ value: 'horizontal', label: t('message.message.multi_model_style.horizontal') },
{ value: 'grid', label: t('message.message.multi_model_style.grid') }
],
[t]
)
const messageNavigationItems = useMemo<SelectorItem<'none' | 'buttons' | 'anchor'>[]>(
() => [
{ value: 'none', label: t('settings.messages.navigation.none') },
{ value: 'buttons', label: t('settings.messages.navigation.buttons') },
{ value: 'anchor', label: t('settings.messages.navigation.anchor') }
],
[t]
)
const mathEngineItems = useMemo<SelectorItem<MathEngine>[]>(
() => [
{ value: 'KaTeX', label: 'KaTeX' },
{ value: 'MathJax', label: 'MathJax' },
{ value: 'none', label: t('settings.math.engine.none') }
],
[t]
)
const codeStyleItems = useMemo<SelectorItem<CodeStyleVarious>[]>(
() => themeNames.map((theme) => ({ value: theme, label: theme })),
[themeNames]
)
const targetLanguageItems = useMemo<SelectorItem<string>[]>(
() => translateLanguages.map((item) => ({ value: item.langCode, label: item.emoji + ' ' + item.label() })),
[translateLanguages]
)
const sendMessageShortcutItems = useMemo<SelectorItem<SendMessageShortcut>[]>(
() => [
{ value: 'Enter', label: getSendMessageShortcutLabel('Enter') },
{ value: 'Ctrl+Enter', label: getSendMessageShortcutLabel('Ctrl+Enter') },
{ value: 'Alt+Enter', label: getSendMessageShortcutLabel('Alt+Enter') },
{ value: 'Command+Enter', label: getSendMessageShortcutLabel('Command+Enter') },
{ value: 'Shift+Enter', label: getSendMessageShortcutLabel('Shift+Enter') }
],
[]
)
const onUpdateAssistantSettings = (settings: Partial<AssistantSettings>) => {
updateAssistantSettings(settings)
}
@ -342,12 +399,10 @@ const SettingsTab: FC<Props> = (props) => {
<Selector
size="sm"
label={t('message.message.style.label')}
selectedKeys={[messageStyle]}
onSelectionChange={(value) => setMessageStyle(value[0] as 'plain' | 'bubble')}
items={[
{ value: 'plain', label: t('message.message.style.plain') },
{ value: 'bubble', label: t('message.message.style.bubble') }
]}
selectionMode="single"
selectedKeys={messageStyle}
onSelectionChange={(value) => setMessageStyle(value)}
items={messageStyleItems}
/>
</SettingRow>
<SettingDivider />
@ -356,14 +411,10 @@ const SettingsTab: FC<Props> = (props) => {
<Selector
size="sm"
label={t('message.message.multi_model_style.label')}
selectedKeys={[multiModelMessageStyle]}
onSelectionChange={(value) => setMultiModelMessageStyle(value[0])}
items={[
{ value: 'fold', label: t('message.message.multi_model_style.fold.label') },
{ value: 'vertical', label: t('message.message.multi_model_style.vertical') },
{ value: 'horizontal', label: t('message.message.multi_model_style.horizontal') },
{ value: 'grid', label: t('message.message.multi_model_style.grid') }
]}
selectionMode="single"
selectedKeys={multiModelMessageStyle}
onSelectionChange={(value) => setMultiModelMessageStyle(value)}
items={multiModelMessageStyleItems}
/>
</SettingRow>
<SettingDivider />
@ -372,13 +423,10 @@ const SettingsTab: FC<Props> = (props) => {
<Selector
size="sm"
label={t('settings.messages.navigation.label')}
selectedKeys={[messageNavigation]}
onSelectionChange={(value) => setMessageNavigation(value[0] as 'none' | 'buttons' | 'anchor')}
items={[
{ value: 'none', label: t('settings.messages.navigation.none') },
{ value: 'buttons', label: t('settings.messages.navigation.buttons') },
{ value: 'anchor', label: t('settings.messages.navigation.anchor') }
]}
selectionMode="single"
selectedKeys={messageNavigation}
onSelectionChange={(value) => setMessageNavigation(value)}
items={messageNavigationItems}
/>
</SettingRow>
<SettingDivider />
@ -412,13 +460,10 @@ const SettingsTab: FC<Props> = (props) => {
<Selector
size="sm"
label={t('settings.math.engine.label')}
selectedKeys={[mathEngine]}
onSelectionChange={(value) => setMathEngine(value[0] as MathEngine)}
items={[
{ value: 'KaTeX', label: 'KaTeX' },
{ value: 'MathJax', label: 'MathJax' },
{ value: 'none', label: t('settings.math.engine.none') }
]}
selectionMode="single"
selectedKeys={mathEngine}
onSelectionChange={(value) => setMathEngine(value)}
items={mathEngineItems}
/>
</SettingRow>
<SettingDivider />
@ -444,12 +489,10 @@ const SettingsTab: FC<Props> = (props) => {
<Selector
size="sm"
label={t('message.message.code_style')}
selectedKeys={[codeStyle]}
onSelectionChange={(value) => onCodeStyleChange(value[0] as CodeStyleVarious)}
items={themeNames.map((theme) => ({
value: theme,
label: theme
}))}
selectionMode="single"
selectedKeys={codeStyle}
onSelectionChange={(value) => onCodeStyleChange(value)}
items={codeStyleItems}
/>
</SettingRow>
<SettingDivider />
@ -682,12 +725,11 @@ const SettingsTab: FC<Props> = (props) => {
<Selector
size="sm"
label={t('settings.input.target_language.label')}
selectedKeys={[targetLanguage]}
onSelectionChange={(value) => setTargetLanguage(value[0])}
selectionMode="single"
selectedKeys={targetLanguage}
onSelectionChange={(value) => setTargetLanguage(value)}
placeholder={UNKNOWN.emoji + ' ' + UNKNOWN.label()}
items={translateLanguages.map((item) => {
return { value: item.langCode, label: item.emoji + ' ' + item.label() }
})}
items={targetLanguageItems}
/>
</SettingRow>
<SettingDivider />
@ -696,15 +738,10 @@ const SettingsTab: FC<Props> = (props) => {
<Selector
size="sm"
label={t('settings.messages.input.send_shortcuts')}
selectedKeys={[sendMessageShortcut]}
onSelectionChange={(value) => setSendMessageShortcut(value[0] as SendMessageShortcut)}
items={[
{ value: 'Enter', label: getSendMessageShortcutLabel('Enter') },
{ value: 'Ctrl+Enter', label: getSendMessageShortcutLabel('Ctrl+Enter') },
{ value: 'Alt+Enter', label: getSendMessageShortcutLabel('Alt+Enter') },
{ value: 'Command+Enter', label: getSendMessageShortcutLabel('Command+Enter') },
{ value: 'Shift+Enter', label: getSendMessageShortcutLabel('Shift+Enter') }
]}
selectionMode="single"
selectedKeys={sendMessageShortcut}
onSelectionChange={(value) => setSendMessageShortcut(value)}
items={sendMessageShortcutItems}
/>
</SettingRow>
</SettingGroup>