Files
BoosAPI/desktop/src/components/shared/RerankModelSelect.tsx
T
boos e0a5f57793 feat(desktop): embed BoosAPI Desktop client as a subproject
Vendor the rebranded BoosAPI Desktop client (Tauri + Vue) into metapi as
a monorepo subdirectory under desktop/. Source only — node_modules,
dist, Cargo target and the upstream .git history are excluded.

desktop/ keeps its own independent Cargo workspace (desktop/src-tauri
with crates/*); it is not part of the backend boosapi crate, so a
`cargo check` at the metapi root still compiles only the backend. Build
the desktop app from desktop/src-tauri per desktop/README.md.

Note: app icons are still placeholder assets pending the final BoosAPI
logo. The updater signing keys live outside the repo and are not
included.
2026-06-22 11:32:57 +08:00

108 lines
3.2 KiB
TypeScript

import { useMemo, useCallback, useEffect } from 'react';
import { Select, theme } from 'antd';
import { ModelIcon } from '@lobehub/icons';
import { useProviderStore } from '@/stores';
import { parseModelValue, useProviderNameMap } from './ModelSelect';
function isRerankModel(model: { model_id: string; model_type?: string }) {
return model.model_type === 'Rerank' || /rerank|colbert/i.test(model.model_id);
}
function useRerankModelOptions() {
const providers = useProviderStore((s) => s.providers);
const fetchProviders = useProviderStore((s) => s.fetchProviders);
useEffect(() => {
if (providers.length === 0) {
void fetchProviders();
}
}, [fetchProviders, providers.length]);
return useMemo(() => {
return providers
.filter((p) => p.enabled)
.map((p) => {
const rerankModels = p.models.filter((m) => m.enabled && isRerankModel(m));
if (rerankModels.length === 0) return null;
return {
label: (
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 6 }}>
<ModelIcon model={p.name} size={16} type="avatar" />
{p.name}
</span>
),
title: p.name,
options: rerankModels.map((m) => ({
label: m.name,
value: `${p.id}::${m.model_id}`,
modelId: m.model_id,
providerName: p.name,
})),
};
})
.filter((opt): opt is NonNullable<typeof opt> => opt !== null);
}, [providers]);
}
export function RerankModelSelect({
value,
onChange,
placeholder,
allowClear = true,
style,
}: {
value?: string;
onChange: (value: string | undefined) => void;
placeholder?: string;
allowClear?: boolean;
style?: React.CSSProperties;
}) {
const { token } = theme.useToken();
const rerankOptions = useRerankModelOptions();
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: 'inline-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={rerankOptions}
style={style}
/>
);
}