mirror of
https://github.com/QuantumNous/new-api.git
synced 2026-04-19 17:48:37 +00:00
This patch standardises how all “model” (and related) `<Select>` components handle searching.
Highlights
• Added a shared helper `modelSelectFilter` to `helpers/utils.js` – performs case-insensitive value-based matching, independent of ReactNode labels.
• Removed the temporary `helpers/selectFilter.js`; all components now import from the core helpers barrel.
• Updated Selects to use the new filter *and* set `autoClearSearchValue={false}` so the query text is preserved after a choice is made.
Affected components
• Channel editor (EditChannelModal) – channel type & model lists
• Tag editor (EditTagModal) – model list
• Token editor (EditTokenModal) – model-limit list
• Playground SettingsPanel – model selector
Benefits
✓ Consistent search behaviour across the app
✓ Better user experience when selecting multiple items
✓ Cleaner codebase with one canonical filter implementation
253 lines
7.9 KiB
JavaScript
253 lines
7.9 KiB
JavaScript
/*
|
|
Copyright (C) 2025 QuantumNous
|
|
|
|
This program is free software: you can redistribute it and/or modify
|
|
it under the terms of the GNU Affero General Public License as
|
|
published by the Free Software Foundation, either version 3 of the
|
|
License, or (at your option) any later version.
|
|
|
|
This program is distributed in the hope that it will be useful,
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
GNU Affero General Public License for more details.
|
|
|
|
You should have received a copy of the GNU Affero General Public License
|
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
|
|
For commercial licensing, please contact support@quantumnous.com
|
|
*/
|
|
|
|
import React from 'react';
|
|
import {
|
|
Card,
|
|
Select,
|
|
Typography,
|
|
Button,
|
|
Switch,
|
|
} from '@douyinfe/semi-ui';
|
|
import {
|
|
Sparkles,
|
|
Users,
|
|
ToggleLeft,
|
|
X,
|
|
Settings,
|
|
} from 'lucide-react';
|
|
import { useTranslation } from 'react-i18next';
|
|
import { renderGroupOption, modelSelectFilter } from '../../helpers';
|
|
import ParameterControl from './ParameterControl';
|
|
import ImageUrlInput from './ImageUrlInput';
|
|
import ConfigManager from './ConfigManager';
|
|
import CustomRequestEditor from './CustomRequestEditor';
|
|
|
|
const SettingsPanel = ({
|
|
inputs,
|
|
parameterEnabled,
|
|
models,
|
|
groups,
|
|
styleState,
|
|
showDebugPanel,
|
|
customRequestMode,
|
|
customRequestBody,
|
|
onInputChange,
|
|
onParameterToggle,
|
|
onCloseSettings,
|
|
onConfigImport,
|
|
onConfigReset,
|
|
onCustomRequestModeChange,
|
|
onCustomRequestBodyChange,
|
|
previewPayload,
|
|
messages,
|
|
}) => {
|
|
const { t } = useTranslation();
|
|
|
|
const currentConfig = {
|
|
inputs,
|
|
parameterEnabled,
|
|
showDebugPanel,
|
|
customRequestMode,
|
|
customRequestBody,
|
|
};
|
|
|
|
return (
|
|
<Card
|
|
className="h-full flex flex-col"
|
|
bordered={false}
|
|
bodyStyle={{
|
|
padding: styleState.isMobile ? '16px' : '24px',
|
|
height: '100%',
|
|
display: 'flex',
|
|
flexDirection: 'column'
|
|
}}
|
|
>
|
|
{/* 标题区域 - 与调试面板保持一致 */}
|
|
<div className="flex items-center justify-between mb-6 flex-shrink-0">
|
|
<div className="flex items-center">
|
|
<div className="w-10 h-10 rounded-full bg-gradient-to-r from-purple-500 to-pink-500 flex items-center justify-center mr-3">
|
|
<Settings size={20} className="text-white" />
|
|
</div>
|
|
<Typography.Title heading={5} className="mb-0">
|
|
{t('模型配置')}
|
|
</Typography.Title>
|
|
</div>
|
|
|
|
{styleState.isMobile && onCloseSettings && (
|
|
<Button
|
|
icon={<X size={16} />}
|
|
onClick={onCloseSettings}
|
|
theme="borderless"
|
|
type="tertiary"
|
|
size="small"
|
|
className="!rounded-lg"
|
|
/>
|
|
)}
|
|
</div>
|
|
|
|
{/* 移动端配置管理 */}
|
|
{styleState.isMobile && (
|
|
<div className="mb-4 flex-shrink-0">
|
|
<ConfigManager
|
|
currentConfig={currentConfig}
|
|
onConfigImport={onConfigImport}
|
|
onConfigReset={onConfigReset}
|
|
styleState={{ ...styleState, isMobile: false }}
|
|
messages={messages}
|
|
/>
|
|
</div>
|
|
)}
|
|
|
|
<div className="space-y-6 overflow-y-auto flex-1 pr-2 model-settings-scroll">
|
|
{/* 自定义请求体编辑器 */}
|
|
<CustomRequestEditor
|
|
customRequestMode={customRequestMode}
|
|
customRequestBody={customRequestBody}
|
|
onCustomRequestModeChange={onCustomRequestModeChange}
|
|
onCustomRequestBodyChange={onCustomRequestBodyChange}
|
|
defaultPayload={previewPayload}
|
|
/>
|
|
|
|
{/* 分组选择 */}
|
|
<div className={customRequestMode ? 'opacity-50' : ''}>
|
|
<div className="flex items-center gap-2 mb-2">
|
|
<Users size={16} className="text-gray-500" />
|
|
<Typography.Text strong className="text-sm">
|
|
{t('分组')}
|
|
</Typography.Text>
|
|
{customRequestMode && (
|
|
<Typography.Text className="text-xs text-orange-600">
|
|
(已在自定义模式中忽略)
|
|
</Typography.Text>
|
|
)}
|
|
</div>
|
|
<Select
|
|
placeholder={t('请选择分组')}
|
|
name='group'
|
|
required
|
|
selection
|
|
onChange={(value) => onInputChange('group', value)}
|
|
value={inputs.group}
|
|
autoComplete='new-password'
|
|
optionList={groups}
|
|
renderOptionItem={renderGroupOption}
|
|
style={{ width: '100%' }}
|
|
dropdownStyle={{ width: '100%', maxWidth: '100%' }}
|
|
className="!rounded-lg"
|
|
disabled={customRequestMode}
|
|
/>
|
|
</div>
|
|
|
|
{/* 模型选择 */}
|
|
<div className={customRequestMode ? 'opacity-50' : ''}>
|
|
<div className="flex items-center gap-2 mb-2">
|
|
<Sparkles size={16} className="text-gray-500" />
|
|
<Typography.Text strong className="text-sm">
|
|
{t('模型')}
|
|
</Typography.Text>
|
|
{customRequestMode && (
|
|
<Typography.Text className="text-xs text-orange-600">
|
|
(已在自定义模式中忽略)
|
|
</Typography.Text>
|
|
)}
|
|
</div>
|
|
<Select
|
|
placeholder={t('请选择模型')}
|
|
name='model'
|
|
required
|
|
selection
|
|
filter={modelSelectFilter}
|
|
autoClearSearchValue={false}
|
|
onChange={(value) => onInputChange('model', value)}
|
|
value={inputs.model}
|
|
autoComplete='new-password'
|
|
optionList={models}
|
|
style={{ width: '100%' }}
|
|
dropdownStyle={{ width: '100%', maxWidth: '100%' }}
|
|
className="!rounded-lg"
|
|
disabled={customRequestMode}
|
|
/>
|
|
</div>
|
|
|
|
{/* 图片URL输入 */}
|
|
<div className={customRequestMode ? 'opacity-50' : ''}>
|
|
<ImageUrlInput
|
|
imageUrls={inputs.imageUrls}
|
|
imageEnabled={inputs.imageEnabled}
|
|
onImageUrlsChange={(urls) => onInputChange('imageUrls', urls)}
|
|
onImageEnabledChange={(enabled) => onInputChange('imageEnabled', enabled)}
|
|
disabled={customRequestMode}
|
|
/>
|
|
</div>
|
|
|
|
{/* 参数控制组件 */}
|
|
<div className={customRequestMode ? 'opacity-50' : ''}>
|
|
<ParameterControl
|
|
inputs={inputs}
|
|
parameterEnabled={parameterEnabled}
|
|
onInputChange={onInputChange}
|
|
onParameterToggle={onParameterToggle}
|
|
disabled={customRequestMode}
|
|
/>
|
|
</div>
|
|
|
|
{/* 流式输出开关 */}
|
|
<div className={customRequestMode ? 'opacity-50' : ''}>
|
|
<div className="flex items-center justify-between">
|
|
<div className="flex items-center gap-2">
|
|
<ToggleLeft size={16} className="text-gray-500" />
|
|
<Typography.Text strong className="text-sm">
|
|
流式输出
|
|
</Typography.Text>
|
|
{customRequestMode && (
|
|
<Typography.Text className="text-xs text-orange-600">
|
|
(已在自定义模式中忽略)
|
|
</Typography.Text>
|
|
)}
|
|
</div>
|
|
<Switch
|
|
checked={inputs.stream}
|
|
onChange={(checked) => onInputChange('stream', checked)}
|
|
checkedText="开"
|
|
uncheckedText="关"
|
|
size="small"
|
|
disabled={customRequestMode}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 桌面端的配置管理放在底部 */}
|
|
{!styleState.isMobile && (
|
|
<div className="flex-shrink-0 pt-3">
|
|
<ConfigManager
|
|
currentConfig={currentConfig}
|
|
onConfigImport={onConfigImport}
|
|
onConfigReset={onConfigReset}
|
|
styleState={styleState}
|
|
messages={messages}
|
|
/>
|
|
</div>
|
|
)}
|
|
</Card>
|
|
);
|
|
};
|
|
|
|
export default SettingsPanel;
|