Skip to content

Commit 81ebdcc

Browse files
authored
Feat search by tag (#61)
* feat:添加按照tag筛选功能 * feat: 为api key 添加密码输入效果
1 parent 6c99862 commit 81ebdcc

File tree

4 files changed

+253
-4
lines changed

4 files changed

+253
-4
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,4 @@ ui/ModelModal/node_modules
44
ui/ModelModal/dist
55
.DS_Store
66
test/ui_example/dist
7+
books

ui/ModelModal/src/ModelModal.tsx

Lines changed: 63 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {
1616
import { Icon, message, Modal, ThemeProvider } from '@c-x/ui';
1717
import Card from './components/card';
1818
import ModelTagsWithLabel from './components/ModelTagsWithLabel';
19+
import ModelTagFilter from './components/ModelTagFilter';
1920
import React, { useEffect, useState } from 'react';
2021
import { useForm, Controller } from 'react-hook-form';
2122
import {
@@ -91,6 +92,7 @@ export const ModelModal: React.FC<ModelModalProps> = ({
9192
const baseUrl = watch('base_url');
9293

9394
const [modelUserList, setModelUserList] = useState<{ model: string }[]>([]);
95+
const [filteredModelList, setFilteredModelList] = useState<{ model: string; provider: string }[]>([]);
9496

9597
const [loading, setLoading] = useState(false);
9698
const [modelLoading, setModelLoading] = useState(false);
@@ -119,6 +121,7 @@ export const ModelModal: React.FC<ModelModalProps> = ({
119121
support_prompt_caching: false,
120122
});
121123
setModelUserList([]);
124+
setFilteredModelList([]);
122125
setSuccess(false);
123126
setLoading(false);
124127
setModelLoading(false);
@@ -630,6 +633,7 @@ export const ModelModal: React.FC<ModelModalProps> = ({
630633
{...field}
631634
fullWidth
632635
size='small'
636+
type='password'
633637
placeholder=''
634638
error={!!errors.api_key}
635639
helperText={errors.api_key?.message}
@@ -767,20 +771,35 @@ export const ModelModal: React.FC<ModelModalProps> = ({
767771
placeholder=''
768772
error={!!errors.model_name}
769773
helperText={errors.model_name?.message}
774+
SelectProps={{
775+
MenuProps: {
776+
PaperProps: {
777+
sx: {
778+
maxHeight: 300,
779+
'& .MuiList-root': {
780+
paddingTop: 0,
781+
}
782+
}
783+
}
784+
}
785+
}}
770786
>
771787
{(() => {
788+
// 使用筛选后的模型列表,如果没有筛选则使用原始列表
789+
const modelsToShow = filteredModelList.length > 0 ? filteredModelList : modelUserList.map(item => ({ model: item.model, provider: providerBrand }));
790+
772791
// 按组分类模型
773-
const groupedModels = modelUserList.reduce((acc, model) => {
792+
const groupedModels = modelsToShow.reduce((acc, model) => {
774793
const group = getModelGroup(model.model);
775794
if (!acc[group]) {
776795
acc[group] = [];
777796
}
778797
acc[group].push(model);
779798
return acc;
780-
}, {} as Record<string, typeof modelUserList>);
799+
}, {} as Record<string, typeof modelsToShow>);
781800

782801
// 渲染分组后的模型
783-
return Object.entries(groupedModels).map(([group, models]) => [
802+
const modelItems = Object.entries(groupedModels).map(([group, models]) => [
784803
<ListSubheader key={`header-${group}`} sx={{ backgroundColor: 'transparent', fontWeight: 'bold', position: 'static' }}>
785804
{group}
786805
</ListSubheader>,
@@ -808,6 +827,47 @@ export const ModelModal: React.FC<ModelModalProps> = ({
808827
</MenuItem>
809828
))
810829
]).flat();
830+
831+
return [
832+
<MenuItem
833+
key="sticky-chip"
834+
disableRipple
835+
disableTouchRipple
836+
sx={{
837+
position: 'sticky',
838+
top: 0,
839+
zIndex: 1000,
840+
backgroundColor: '#ffffff !important',
841+
borderBottom: '1px solid',
842+
borderColor: 'divider',
843+
boxShadow: '0 2px 4px rgba(0,0,0,0.1)',
844+
'&:hover': {
845+
backgroundColor: '#ffffff !important'
846+
},
847+
'&:focus': {
848+
backgroundColor: '#ffffff !important'
849+
},
850+
cursor: 'default',
851+
'&.Mui-selected': {
852+
backgroundColor: '#ffffff !important'
853+
},
854+
'&.Mui-selected:hover': {
855+
backgroundColor: '#ffffff !important'
856+
}
857+
}}
858+
onClick={(e) => {
859+
e.preventDefault();
860+
}}
861+
>
862+
<ModelTagFilter
863+
models={modelUserList.map(item => ({ model: item.model, provider: providerBrand }))}
864+
onFilteredModelsChange={(filteredModels) => {
865+
setFilteredModelList(filteredModels);
866+
}}
867+
/>
868+
</MenuItem>,
869+
...modelItems
870+
];
811871
})()}
812872
</TextField>
813873
)}
Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
import React, { useState, useMemo } from 'react';
2+
import { Box, Chip, Stack } from '@mui/material';
3+
import { useTheme, alpha as addOpacityToColor } from '@mui/material';
4+
import {
5+
isVisionModel,
6+
isWebSearchModel,
7+
isReasoningModel,
8+
isFunctionCallingModel,
9+
isCodeModel,
10+
isEmbeddingModel,
11+
isRerankModel
12+
} from '../utils/model';
13+
14+
interface ModelTagFilterProps {
15+
models: Array<{
16+
model: string;
17+
provider: string;
18+
[key: string]: any;
19+
}>;
20+
onFilteredModelsChange: (filteredModels: any[]) => void;
21+
selectedTags?: string[];
22+
onSelectedTagsChange?: (tags: string[]) => void;
23+
}
24+
25+
const ModelTagFilter: React.FC<ModelTagFilterProps> = ({
26+
models,
27+
onFilteredModelsChange,
28+
selectedTags = [],
29+
onSelectedTagsChange
30+
}) => {
31+
const theme = useTheme();
32+
const [internalSelectedTags, setInternalSelectedTags] = useState<string[]>([]);
33+
34+
// 使用外部传入的selectedTags或内部状态
35+
const currentSelectedTags = selectedTags.length > 0 ? selectedTags : internalSelectedTags;
36+
37+
// 定义可用的标签类型
38+
const availableTags = [
39+
{ key: 'all', label: '全部', color: 'default' as const },
40+
{ key: 'reasoning', label: '深度思考', color: 'primary' as const },
41+
{ key: 'vision', label: '视觉', color: 'secondary' as const },
42+
{ key: 'function_calling', label: '工具调用', color: 'success' as const },
43+
{ key: 'code', label: '代码生成', color: 'warning' as const },
44+
{ key: 'embedding', label: '向量', color: 'error' as const },
45+
{ key: 'rerank', label: '重排', color: 'default' as const }
46+
];
47+
48+
// 根据模型计算每个标签的可用性
49+
const tagStats = useMemo(() => {
50+
const stats: Record<string, { available: boolean }> = {};
51+
52+
availableTags.forEach(tag => {
53+
if (tag.key === 'all') {
54+
stats[tag.key] = { available: true };
55+
return;
56+
}
57+
58+
const count = models.filter(model => {
59+
switch (tag.key) {
60+
case 'reasoning':
61+
return isReasoningModel(model.model, model.provider);
62+
case 'vision':
63+
return isVisionModel(model.model, model.provider);
64+
case 'websearch':
65+
return isWebSearchModel(model.model, model.provider);
66+
case 'function_calling':
67+
return isFunctionCallingModel(model.model, model.provider);
68+
case 'code':
69+
return isCodeModel(model.model, model.provider);
70+
case 'embedding':
71+
return isEmbeddingModel(model.model, model.provider);
72+
case 'rerank':
73+
return isRerankModel(model.model);
74+
default:
75+
return false;
76+
}
77+
}).length;
78+
79+
stats[tag.key] = { available: count > 0 };
80+
});
81+
82+
return stats;
83+
}, [models]);
84+
85+
// 根据选中的标签筛选模型
86+
const filteredModels = useMemo(() => {
87+
if (currentSelectedTags.length === 0 || currentSelectedTags.includes('all')) {
88+
return models;
89+
}
90+
91+
return models.filter(model => {
92+
return currentSelectedTags.some(tag => {
93+
switch (tag) {
94+
case 'reasoning':
95+
return isReasoningModel(model.model, model.provider);
96+
case 'vision':
97+
return isVisionModel(model.model, model.provider);
98+
case 'websearch':
99+
return isWebSearchModel(model.model, model.provider);
100+
case 'function_calling':
101+
return isFunctionCallingModel(model.model, model.provider);
102+
case 'code':
103+
return isCodeModel(model.model, model.provider);
104+
case 'embedding':
105+
return isEmbeddingModel(model.model, model.provider);
106+
case 'rerank':
107+
return isRerankModel(model.model);
108+
default:
109+
return false;
110+
}
111+
});
112+
});
113+
}, [models, currentSelectedTags]);
114+
115+
// 当筛选结果变化时通知父组件
116+
React.useEffect(() => {
117+
onFilteredModelsChange(filteredModels);
118+
}, [filteredModels, onFilteredModelsChange]);
119+
120+
const handleTagClick = (tagKey: string) => {
121+
let newSelectedTags: string[];
122+
123+
if (tagKey === 'all') {
124+
newSelectedTags = [];
125+
} else {
126+
// 单选逻辑:如果点击的是已选中的tag,则取消选择;否则只选择这个tag
127+
if (currentSelectedTags.includes(tagKey)) {
128+
newSelectedTags = [];
129+
} else {
130+
newSelectedTags = [tagKey];
131+
}
132+
}
133+
134+
if (onSelectedTagsChange) {
135+
onSelectedTagsChange(newSelectedTags);
136+
} else {
137+
setInternalSelectedTags(newSelectedTags);
138+
}
139+
};
140+
141+
const isTagSelected = (tagKey: string) => {
142+
if (tagKey === 'all') {
143+
return currentSelectedTags.length === 0 || currentSelectedTags.includes('all');
144+
}
145+
return currentSelectedTags.includes(tagKey);
146+
};
147+
148+
return (
149+
<Box>
150+
<Stack direction="row" spacing={1} flexWrap="wrap" useFlexGap>
151+
{availableTags.map(tag => {
152+
const stats = tagStats[tag.key];
153+
const selected = isTagSelected(tag.key);
154+
155+
return (
156+
<Chip
157+
key={tag.key}
158+
label={tag.label}
159+
variant={selected ? 'filled' : 'outlined'}
160+
color={selected ? tag.color : 'default'}
161+
size="small"
162+
disabled={!stats.available}
163+
onClick={(e) => {
164+
e.stopPropagation();
165+
handleTagClick(tag.key);
166+
}}
167+
sx={{
168+
mb: 0.5,
169+
cursor: stats.available ? 'pointer' : 'not-allowed',
170+
'&:hover': stats.available ? {
171+
backgroundColor: selected
172+
? addOpacityToColor(theme.palette[tag.color === 'default' ? 'primary' : tag.color].main, 0.8)
173+
: addOpacityToColor(theme.palette[tag.color === 'default' ? 'primary' : tag.color].main, 0.1)
174+
} : {},
175+
...(selected && {
176+
backgroundColor: theme.palette[tag.color === 'default' ? 'primary' : tag.color].main,
177+
color: theme.palette[tag.color === 'default' ? 'primary' : tag.color].contrastText
178+
})
179+
}}
180+
/>
181+
);
182+
})}
183+
</Stack>
184+
</Box>
185+
);
186+
};
187+
188+
export default ModelTagFilter;

ui/ModelModal/src/components/Tags/ModelCapabilities/EmbeddingTag.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,5 @@ type Props = {
88

99
export const EmbeddingTag = ({ size, ...restProps }: Props) => {
1010
const { t } = useTranslation()
11-
return <CustomTag size={size} color="#FFA500" icon="嵌入" {...restProps} />
11+
return <CustomTag size={size} color="#FFA500" icon="向量" {...restProps} />
1212
}

0 commit comments

Comments
 (0)