-
Notifications
You must be signed in to change notification settings - Fork 62
Expand file tree
/
Copy pathModelSelect.tsx
More file actions
121 lines (114 loc) · 3.74 KB
/
ModelSelect.tsx
File metadata and controls
121 lines (114 loc) · 3.74 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
import { useMemo, useCallback } from 'react';
import { Select, theme } from 'antd';
import { ModelIcon } from '@lobehub/icons';
import { useProviderStore } from '@/stores';
import { SmartProviderIcon } from '@/lib/providerIcons';
import type { ModelType } from '@/types';
/** Parse a combined `providerId::modelId` value. */
export function parseModelValue(value: string | undefined) {
if (!value) return null;
const idx = value.indexOf('::');
if (idx < 0) return null;
return { providerId: value.slice(0, idx), modelId: value.slice(idx + 2) };
}
/** Hook: returns grouped Select options (Provider → Models) */
export function useGroupedModelOptions(modelType?: ModelType) {
const providers = useProviderStore((s) => s.providers);
return useMemo(() => {
return providers
.filter((p) => p.enabled)
.map((p) => {
const models = p.models.filter((m) => m.enabled && (!modelType || m.model_type === modelType));
if (models.length === 0) return null;
return {
label: (
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 6 }}>
<SmartProviderIcon provider={p} size={16} type="avatar" />
{p.name}
</span>
),
title: p.name,
options: models.map((m) => ({
label: m.name,
value: `${p.id}::${m.model_id}`,
modelId: m.model_id,
providerName: p.name,
})),
};
})
.filter((option): option is NonNullable<typeof option> => option !== null);
}, [providers, modelType]);
}
/** Hook: returns Map<providerId, providerName> */
export function useProviderNameMap() {
const providers = useProviderStore((s) => s.providers);
return useMemo(() => {
const map = new Map<string, string>();
providers.forEach((p) => map.set(p.id, p.name));
return map;
}, [providers]);
}
/**
* Reusable model selector with provider-grouped options, ModelIcon rendering,
* and search support. Value format: `providerId::modelId`.
*/
export function ModelSelect({
value,
onChange,
placeholder,
allowClear = true,
style,
modelType,
}: {
value?: string;
onChange: (value: string | undefined) => void;
placeholder?: string;
allowClear?: boolean;
style?: React.CSSProperties;
modelType?: ModelType;
}) {
const { token } = theme.useToken();
const groupedOptions = useGroupedModelOptions(modelType);
const providerNameMap = useProviderNameMap();
const optionRender = useCallback(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(option: any) => (
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 6 }}>
<ModelIcon model={option.data?.modelId ?? ''} size={18} type="avatar" />
{option.label}
</span>
),
[],
);
const labelRender = useCallback(
(props: { label?: React.ReactNode; value?: string | number }) => {
const parsed = parseModelValue(String(props.value ?? ''));
if (!parsed) return <span>{props.label}</span>;
const providerName = providerNameMap.get(parsed.providerId) ?? '';
return (
<span style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<ModelIcon model={parsed.modelId} size={18} type="avatar" />
{props.label}
<span style={{ fontSize: 11, color: token.colorTextSecondary }}>
({providerName})
</span>
</span>
);
},
[providerNameMap, token.colorTextSecondary],
);
return (
<Select
value={value}
onChange={onChange}
placeholder={placeholder}
allowClear={allowClear}
showSearch
optionFilterProp="label"
optionRender={optionRender}
labelRender={labelRender}
options={groupedOptions}
style={style}
/>
);
}