mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2026-01-09 23:10:20 +08:00
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:
parent
6c71b92d1d
commit
db4fcac768
@ -130,7 +130,7 @@ export interface PreferenceSchemas {
|
|||||||
// redux/settings/fontSize
|
// redux/settings/fontSize
|
||||||
'chat.message.font_size': number
|
'chat.message.font_size': number
|
||||||
// redux/settings/mathEngine
|
// redux/settings/mathEngine
|
||||||
'chat.message.math.engine': string
|
'chat.message.math.engine': PreferenceTypes.MathEngine
|
||||||
// redux/settings/mathEnableSingleDollar
|
// redux/settings/mathEnableSingleDollar
|
||||||
'chat.message.math.single_dollar': boolean
|
'chat.message.math.single_dollar': boolean
|
||||||
// redux/settings/foldDisplayMode
|
// redux/settings/foldDisplayMode
|
||||||
|
|||||||
@ -70,6 +70,8 @@ export type ProxyMode = 'system' | 'custom' | 'none'
|
|||||||
|
|
||||||
export type MultiModelFoldDisplayMode = 'expanded' | 'compact'
|
export type MultiModelFoldDisplayMode = 'expanded' | 'compact'
|
||||||
|
|
||||||
|
export type MathEngine = 'KaTeX' | 'MathJax' | 'none'
|
||||||
|
|
||||||
export enum UpgradeChannel {
|
export enum UpgradeChannel {
|
||||||
LATEST = 'latest', // 最新稳定版本
|
LATEST = 'latest', // 最新稳定版本
|
||||||
RC = 'rc', // 公测版本
|
RC = 'rc', // 公测版本
|
||||||
|
|||||||
333
packages/ui/src/components/base/Selector/README.md
Normal file
333
packages/ui/src/components/base/Selector/README.md
Normal 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} />
|
||||||
|
```
|
||||||
@ -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
|
||||||
75
packages/ui/src/components/base/Selector/Selector.tsx
Normal file
75
packages/ui/src/components/base/Selector/Selector.tsx
Normal 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
|
||||||
@ -1,51 +1,13 @@
|
|||||||
import type { Selection, SelectProps } from '@heroui/react'
|
// 统一导出 Selector 相关组件和类型
|
||||||
import { Select, SelectItem } from '@heroui/react'
|
export { default as SearchableSelector } from './SearchableSelector'
|
||||||
import type { ReactNode } from 'react'
|
export { default } from './Selector'
|
||||||
|
export type {
|
||||||
interface SelectorItem<V = string | number> {
|
MultipleSearchableSelectorProps,
|
||||||
label: string | ReactNode
|
MultipleSelectorProps,
|
||||||
value: V
|
SearchableSelectorItem,
|
||||||
disabled?: boolean
|
SearchableSelectorProps,
|
||||||
[key: string]: any
|
SelectorItem,
|
||||||
}
|
SelectorProps,
|
||||||
|
SingleSearchableSelectorProps,
|
||||||
interface SelectorProps<V = string | number> extends Omit<SelectProps, 'children' | 'onSelectionChange'> {
|
SingleSelectorProps
|
||||||
items: SelectorItem<V>[]
|
} from './types'
|
||||||
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 }
|
|
||||||
|
|||||||
79
packages/ui/src/components/base/Selector/types.ts
Normal file
79
packages/ui/src/components/base/Selector/types.ts
Normal 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
|
||||||
|
}
|
||||||
@ -1,5 +1,5 @@
|
|||||||
// Base Components
|
// 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 Button, type ButtonProps } from './base/Button'
|
||||||
export { default as CopyButton } from './base/CopyButton'
|
export { default as CopyButton } from './base/CopyButton'
|
||||||
export { default as CustomCollapse } from './base/CustomCollapse'
|
export { default as CustomCollapse } from './base/CustomCollapse'
|
||||||
@ -48,8 +48,22 @@ export {
|
|||||||
export { default as SvgSpinners180Ring } from './icons/SvgSpinners180Ring'
|
export { default as SvgSpinners180Ring } from './icons/SvgSpinners180Ring'
|
||||||
export { default as ToolsCallingIcon } from './icons/ToolsCallingIcon'
|
export { default as ToolsCallingIcon } from './icons/ToolsCallingIcon'
|
||||||
|
|
||||||
// Interactive Components
|
/* Interactive Components */
|
||||||
|
|
||||||
|
// Selector / SearchableSelector
|
||||||
export { default as Selector } from './base/Selector'
|
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 {
|
export {
|
||||||
default as CodeEditor,
|
default as CodeEditor,
|
||||||
type CodeEditorHandles,
|
type CodeEditorHandles,
|
||||||
@ -58,14 +72,22 @@ export {
|
|||||||
getCmThemeByName,
|
getCmThemeByName,
|
||||||
getCmThemeNames
|
getCmThemeNames
|
||||||
} from './interactive/CodeEditor'
|
} from './interactive/CodeEditor'
|
||||||
|
// CollapsibleSearchBar
|
||||||
export { default as CollapsibleSearchBar } from './interactive/CollapsibleSearchBar'
|
export { default as CollapsibleSearchBar } from './interactive/CollapsibleSearchBar'
|
||||||
|
// DraggableList
|
||||||
export { DraggableList, useDraggableReorder } from './interactive/DraggableList'
|
export { DraggableList, useDraggableReorder } from './interactive/DraggableList'
|
||||||
|
// EditableNumber
|
||||||
export type { EditableNumberProps } from './interactive/EditableNumber'
|
export type { EditableNumberProps } from './interactive/EditableNumber'
|
||||||
|
// EditableNumber
|
||||||
export { default as EditableNumber } from './interactive/EditableNumber'
|
export { default as EditableNumber } from './interactive/EditableNumber'
|
||||||
export { default as HelpTooltip } from './interactive/HelpTooltip'
|
export { default as HelpTooltip } from './interactive/HelpTooltip'
|
||||||
|
// ImageToolButton
|
||||||
export { default as ImageToolButton } from './interactive/ImageToolButton'
|
export { default as ImageToolButton } from './interactive/ImageToolButton'
|
||||||
|
// InfoTooltip
|
||||||
export { default as InfoTooltip } from './interactive/InfoTooltip'
|
export { default as InfoTooltip } from './interactive/InfoTooltip'
|
||||||
|
// Sortable
|
||||||
export { Sortable } from './interactive/Sortable'
|
export { Sortable } from './interactive/Sortable'
|
||||||
|
// WarnTooltip
|
||||||
export { default as WarnTooltip } from './interactive/WarnTooltip'
|
export { default as WarnTooltip } from './interactive/WarnTooltip'
|
||||||
|
|
||||||
// Composite Components (复合组件)
|
// Composite Components (复合组件)
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import type { Meta, StoryObj } from '@storybook/react'
|
import type { Meta } from '@storybook/react'
|
||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
|
|
||||||
import Selector from '../../../src/components/base/Selector'
|
import Selector from '../../../src/components/base/Selector'
|
||||||
@ -49,18 +49,18 @@ const meta: Meta<typeof Selector> = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default meta
|
export default meta
|
||||||
type Story = StoryObj<typeof meta>
|
|
||||||
|
|
||||||
// 基础用法
|
// 基础用法 - 单选
|
||||||
export const Default: Story = {
|
export const Default = {
|
||||||
render: function Render() {
|
render: function Render() {
|
||||||
const [selectedKeys, setSelectedKeys] = useState<Set<string>>(new Set(['react']))
|
const [selectedValue, setSelectedValue] = useState<string>('react')
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<Selector
|
<Selector
|
||||||
selectedKeys={selectedKeys}
|
selectionMode="single"
|
||||||
onSelectionChange={(keys) => setSelectedKeys(new Set(keys.map(String)))}
|
selectedKeys={selectedValue}
|
||||||
|
onSelectionChange={(value) => setSelectedValue(value)}
|
||||||
placeholder="选择框架"
|
placeholder="选择框架"
|
||||||
items={[
|
items={[
|
||||||
{ value: 'react', label: 'React' },
|
{ value: 'react', label: 'React' },
|
||||||
@ -70,7 +70,7 @@ export const Default: Story = {
|
|||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
<div className="text-sm text-gray-600">
|
<div className="text-sm text-gray-600">
|
||||||
当前选择: <code>{Array.from(selectedKeys).join(', ')}</code>
|
当前选择: <code>{selectedValue}</code>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
@ -78,16 +78,16 @@ export const Default: Story = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 多选模式
|
// 多选模式
|
||||||
export const Multiple: Story = {
|
export const Multiple = {
|
||||||
render: function Render() {
|
render: function Render() {
|
||||||
const [selectedKeys, setSelectedKeys] = useState<Set<string>>(new Set(['react', 'vue']))
|
const [selectedValues, setSelectedValues] = useState<string[]>(['react', 'vue'])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<Selector
|
<Selector
|
||||||
selectionMode="multiple"
|
selectionMode="multiple"
|
||||||
selectedKeys={selectedKeys}
|
selectedKeys={selectedValues}
|
||||||
onSelectionChange={(keys) => setSelectedKeys(new Set(keys.map(String)))}
|
onSelectionChange={(values) => setSelectedValues(values)}
|
||||||
placeholder="选择多个框架"
|
placeholder="选择多个框架"
|
||||||
items={[
|
items={[
|
||||||
{ value: 'react', label: 'React' },
|
{ value: 'react', label: 'React' },
|
||||||
@ -98,7 +98,7 @@ export const Multiple: Story = {
|
|||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
<div className="text-sm text-gray-600">
|
<div className="text-sm text-gray-600">
|
||||||
已选择 ({selectedKeys.size}): {Array.from(selectedKeys).join(', ')}
|
已选择 ({selectedValues.length}): {selectedValues.join(', ')}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
@ -106,19 +106,16 @@ export const Multiple: Story = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 数字值类型
|
// 数字值类型
|
||||||
export const NumberValues: Story = {
|
export const NumberValues = {
|
||||||
render: function Render() {
|
render: function Render() {
|
||||||
const [selectedKeys, setSelectedKeys] = useState<Set<string>>(new Set(['2']))
|
|
||||||
const [selectedValue, setSelectedValue] = useState<number>(2)
|
const [selectedValue, setSelectedValue] = useState<number>(2)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<Selector
|
<Selector
|
||||||
selectedKeys={selectedKeys}
|
selectionMode="single"
|
||||||
onSelectionChange={(keys) => {
|
selectedKeys={selectedValue}
|
||||||
setSelectedKeys(new Set(keys.map(String)))
|
onSelectionChange={(value) => setSelectedValue(value)}
|
||||||
setSelectedValue(keys[0] as number)
|
|
||||||
}}
|
|
||||||
placeholder="选择优先级"
|
placeholder="选择优先级"
|
||||||
items={[
|
items={[
|
||||||
{ value: 1, label: '🔴 紧急' },
|
{ value: 1, label: '🔴 紧急' },
|
||||||
@ -136,7 +133,7 @@ export const NumberValues: Story = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 不同大小
|
// 不同大小
|
||||||
export const Sizes: Story = {
|
export const Sizes = {
|
||||||
render: function Render() {
|
render: function Render() {
|
||||||
const items = [
|
const items = [
|
||||||
{ value: 'option1', label: '选项 1' },
|
{ value: 'option1', label: '选项 1' },
|
||||||
@ -164,22 +161,26 @@ export const Sizes: Story = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 禁用状态
|
// 禁用状态
|
||||||
export const Disabled: Story = {
|
export const Disabled = {
|
||||||
args: {
|
render: function Render() {
|
||||||
isDisabled: true,
|
return (
|
||||||
selectedKeys: new Set(['react']),
|
<Selector
|
||||||
placeholder: '禁用的选择器',
|
isDisabled
|
||||||
items: [
|
selectedKeys="react"
|
||||||
{ value: 'react', label: 'React' },
|
placeholder="禁用的选择器"
|
||||||
{ value: 'vue', label: 'Vue' }
|
items={[
|
||||||
]
|
{ value: 'react', label: 'React' },
|
||||||
|
{ value: 'vue', label: 'Vue' }
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 实际应用场景:语言选择
|
// 实际应用场景:语言选择
|
||||||
export const LanguageSelector: Story = {
|
export const LanguageSelector = {
|
||||||
render: function Render() {
|
render: function Render() {
|
||||||
const [selectedKeys, setSelectedKeys] = useState<Set<string>>(new Set(['zh']))
|
const [selectedValue, setSelectedValue] = useState<string>('zh')
|
||||||
|
|
||||||
const languages = [
|
const languages = [
|
||||||
{ value: 'zh', label: '🇨🇳 简体中文' },
|
{ value: 'zh', label: '🇨🇳 简体中文' },
|
||||||
@ -193,13 +194,14 @@ export const LanguageSelector: Story = {
|
|||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<Selector
|
<Selector
|
||||||
selectedKeys={selectedKeys}
|
selectionMode="single"
|
||||||
onSelectionChange={(keys) => setSelectedKeys(new Set(keys.map(String)))}
|
selectedKeys={selectedValue}
|
||||||
|
onSelectionChange={(value) => setSelectedValue(value)}
|
||||||
placeholder="选择语言"
|
placeholder="选择语言"
|
||||||
items={languages}
|
items={languages}
|
||||||
/>
|
/>
|
||||||
<div className="p-4 bg-gray-100 dark:bg-gray-800 rounded">
|
<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>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -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 { useMultiplePreferences, usePreference } from '@data/hooks/usePreference'
|
||||||
import EditableNumber from '@renderer/components/EditableNumber'
|
import EditableNumber from '@renderer/components/EditableNumber'
|
||||||
import Scrollbar from '@renderer/components/Scrollbar'
|
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 type { Assistant, AssistantSettings, CodeStyleVarious, MathEngine } from '@renderer/types'
|
||||||
import { modalConfirm } from '@renderer/utils'
|
import { modalConfirm } from '@renderer/utils'
|
||||||
import { getSendMessageShortcutLabel } from '@renderer/utils/input'
|
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 { ThemeMode } from '@shared/data/preference/preferenceTypes'
|
||||||
import { Col, InputNumber, Row, Slider } from 'antd'
|
import { Col, InputNumber, Row, Slider } from 'antd'
|
||||||
import { Settings2 } from 'lucide-react'
|
import { Settings2 } from 'lucide-react'
|
||||||
@ -101,6 +101,63 @@ const SettingsTab: FC<Props> = (props) => {
|
|||||||
|
|
||||||
const { t } = useTranslation()
|
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>) => {
|
const onUpdateAssistantSettings = (settings: Partial<AssistantSettings>) => {
|
||||||
updateAssistantSettings(settings)
|
updateAssistantSettings(settings)
|
||||||
}
|
}
|
||||||
@ -342,12 +399,10 @@ const SettingsTab: FC<Props> = (props) => {
|
|||||||
<Selector
|
<Selector
|
||||||
size="sm"
|
size="sm"
|
||||||
label={t('message.message.style.label')}
|
label={t('message.message.style.label')}
|
||||||
selectedKeys={[messageStyle]}
|
selectionMode="single"
|
||||||
onSelectionChange={(value) => setMessageStyle(value[0] as 'plain' | 'bubble')}
|
selectedKeys={messageStyle}
|
||||||
items={[
|
onSelectionChange={(value) => setMessageStyle(value)}
|
||||||
{ value: 'plain', label: t('message.message.style.plain') },
|
items={messageStyleItems}
|
||||||
{ value: 'bubble', label: t('message.message.style.bubble') }
|
|
||||||
]}
|
|
||||||
/>
|
/>
|
||||||
</SettingRow>
|
</SettingRow>
|
||||||
<SettingDivider />
|
<SettingDivider />
|
||||||
@ -356,14 +411,10 @@ const SettingsTab: FC<Props> = (props) => {
|
|||||||
<Selector
|
<Selector
|
||||||
size="sm"
|
size="sm"
|
||||||
label={t('message.message.multi_model_style.label')}
|
label={t('message.message.multi_model_style.label')}
|
||||||
selectedKeys={[multiModelMessageStyle]}
|
selectionMode="single"
|
||||||
onSelectionChange={(value) => setMultiModelMessageStyle(value[0])}
|
selectedKeys={multiModelMessageStyle}
|
||||||
items={[
|
onSelectionChange={(value) => setMultiModelMessageStyle(value)}
|
||||||
{ value: 'fold', label: t('message.message.multi_model_style.fold.label') },
|
items={multiModelMessageStyleItems}
|
||||||
{ 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') }
|
|
||||||
]}
|
|
||||||
/>
|
/>
|
||||||
</SettingRow>
|
</SettingRow>
|
||||||
<SettingDivider />
|
<SettingDivider />
|
||||||
@ -372,13 +423,10 @@ const SettingsTab: FC<Props> = (props) => {
|
|||||||
<Selector
|
<Selector
|
||||||
size="sm"
|
size="sm"
|
||||||
label={t('settings.messages.navigation.label')}
|
label={t('settings.messages.navigation.label')}
|
||||||
selectedKeys={[messageNavigation]}
|
selectionMode="single"
|
||||||
onSelectionChange={(value) => setMessageNavigation(value[0] as 'none' | 'buttons' | 'anchor')}
|
selectedKeys={messageNavigation}
|
||||||
items={[
|
onSelectionChange={(value) => setMessageNavigation(value)}
|
||||||
{ value: 'none', label: t('settings.messages.navigation.none') },
|
items={messageNavigationItems}
|
||||||
{ value: 'buttons', label: t('settings.messages.navigation.buttons') },
|
|
||||||
{ value: 'anchor', label: t('settings.messages.navigation.anchor') }
|
|
||||||
]}
|
|
||||||
/>
|
/>
|
||||||
</SettingRow>
|
</SettingRow>
|
||||||
<SettingDivider />
|
<SettingDivider />
|
||||||
@ -412,13 +460,10 @@ const SettingsTab: FC<Props> = (props) => {
|
|||||||
<Selector
|
<Selector
|
||||||
size="sm"
|
size="sm"
|
||||||
label={t('settings.math.engine.label')}
|
label={t('settings.math.engine.label')}
|
||||||
selectedKeys={[mathEngine]}
|
selectionMode="single"
|
||||||
onSelectionChange={(value) => setMathEngine(value[0] as MathEngine)}
|
selectedKeys={mathEngine}
|
||||||
items={[
|
onSelectionChange={(value) => setMathEngine(value)}
|
||||||
{ value: 'KaTeX', label: 'KaTeX' },
|
items={mathEngineItems}
|
||||||
{ value: 'MathJax', label: 'MathJax' },
|
|
||||||
{ value: 'none', label: t('settings.math.engine.none') }
|
|
||||||
]}
|
|
||||||
/>
|
/>
|
||||||
</SettingRow>
|
</SettingRow>
|
||||||
<SettingDivider />
|
<SettingDivider />
|
||||||
@ -444,12 +489,10 @@ const SettingsTab: FC<Props> = (props) => {
|
|||||||
<Selector
|
<Selector
|
||||||
size="sm"
|
size="sm"
|
||||||
label={t('message.message.code_style')}
|
label={t('message.message.code_style')}
|
||||||
selectedKeys={[codeStyle]}
|
selectionMode="single"
|
||||||
onSelectionChange={(value) => onCodeStyleChange(value[0] as CodeStyleVarious)}
|
selectedKeys={codeStyle}
|
||||||
items={themeNames.map((theme) => ({
|
onSelectionChange={(value) => onCodeStyleChange(value)}
|
||||||
value: theme,
|
items={codeStyleItems}
|
||||||
label: theme
|
|
||||||
}))}
|
|
||||||
/>
|
/>
|
||||||
</SettingRow>
|
</SettingRow>
|
||||||
<SettingDivider />
|
<SettingDivider />
|
||||||
@ -682,12 +725,11 @@ const SettingsTab: FC<Props> = (props) => {
|
|||||||
<Selector
|
<Selector
|
||||||
size="sm"
|
size="sm"
|
||||||
label={t('settings.input.target_language.label')}
|
label={t('settings.input.target_language.label')}
|
||||||
selectedKeys={[targetLanguage]}
|
selectionMode="single"
|
||||||
onSelectionChange={(value) => setTargetLanguage(value[0])}
|
selectedKeys={targetLanguage}
|
||||||
|
onSelectionChange={(value) => setTargetLanguage(value)}
|
||||||
placeholder={UNKNOWN.emoji + ' ' + UNKNOWN.label()}
|
placeholder={UNKNOWN.emoji + ' ' + UNKNOWN.label()}
|
||||||
items={translateLanguages.map((item) => {
|
items={targetLanguageItems}
|
||||||
return { value: item.langCode, label: item.emoji + ' ' + item.label() }
|
|
||||||
})}
|
|
||||||
/>
|
/>
|
||||||
</SettingRow>
|
</SettingRow>
|
||||||
<SettingDivider />
|
<SettingDivider />
|
||||||
@ -696,15 +738,10 @@ const SettingsTab: FC<Props> = (props) => {
|
|||||||
<Selector
|
<Selector
|
||||||
size="sm"
|
size="sm"
|
||||||
label={t('settings.messages.input.send_shortcuts')}
|
label={t('settings.messages.input.send_shortcuts')}
|
||||||
selectedKeys={[sendMessageShortcut]}
|
selectionMode="single"
|
||||||
onSelectionChange={(value) => setSendMessageShortcut(value[0] as SendMessageShortcut)}
|
selectedKeys={sendMessageShortcut}
|
||||||
items={[
|
onSelectionChange={(value) => setSendMessageShortcut(value)}
|
||||||
{ value: 'Enter', label: getSendMessageShortcutLabel('Enter') },
|
items={sendMessageShortcutItems}
|
||||||
{ 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') }
|
|
||||||
]}
|
|
||||||
/>
|
/>
|
||||||
</SettingRow>
|
</SettingRow>
|
||||||
</SettingGroup>
|
</SettingGroup>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user