File size: 4,614 Bytes
0b9dc2e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
import { ChevronDown, PlusCircle } from 'lucide-react';

import type { EmbeddingModelCard, KbEmbeddingProvider } from '@/api';
import { Button } from '@/components/ui/button';
import {
	DropdownMenu,
	DropdownMenuContent,
	DropdownMenuGroup,
	DropdownMenuItem,
	DropdownMenuLabel,
	DropdownMenuSeparator,
	DropdownMenuSub,
	DropdownMenuSubContent,
	DropdownMenuSubTrigger,
	DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import { useTranslation } from '@/i18n/useI18n.ts';

interface SelectedEmbedding {
	type: string;
	credentialId: string;
	model: string;
	card: EmbeddingModelCard;
}

interface Props {
	value?: { type: string; credential_id: string; model: string } | null;
	providers: KbEmbeddingProvider[];
	loading?: boolean;
	onChange?: (selected: SelectedEmbedding) => void;
	onAddCredential?: () => void;
	/** Override the trigger label shown when no model is selected. */
	placeholder?: string;
}

/**

 * Embedding model picker. Consumes a pre-grouped, server-filtered

 * list of providers (one entry per credential) and emits the chosen

 * model's full :class:`EmbeddingModelCard` so the caller can derive

 * the dimension separately.

 */
export function EmbeddingSelect({

	value,

	providers,

	loading,

	onChange,

	onAddCredential,

	placeholder,

}: Props) {
	const { t } = useTranslation();

	// Group providers by credential type for the existing two-level UI
	// (provider type → credential → models).
	const groups: Record<string, KbEmbeddingProvider[]> = {};
	for (const provider of providers) {
		const type = (provider.credential.data.type as string) ?? 'unknown';
		if (!groups[type]) groups[type] = [];
		groups[type].push(provider);
	}
	const hasOptions = Object.keys(groups).length > 0;

	const handleSelect = (type: string, credentialId: string, card: EmbeddingModelCard) => {
		onChange?.({ type, credentialId, model: card.name, card });
	};

	const displayLabel = value?.model
		? value.model
		: loading
			? t('embedding-select.loading')
			: (placeholder ?? t('embedding-select.placeholder'));

	return (
		<DropdownMenu>

			<DropdownMenuTrigger asChild>

				<Button variant="outline" size="sm" className="justify-between gap-1">

					<span className="truncate">{displayLabel}</span>

					<ChevronDown className="size-3.5 opacity-50" />

				</Button>

			</DropdownMenuTrigger>

			<DropdownMenuContent align="start" className="min-w-48 max-h-72 overflow-y-auto">

				{!loading && !hasOptions ? (

					<div className="px-2 py-3 text-center text-sm text-muted-foreground">

						<p className="font-medium">{t('embedding-select.empty.title')}</p>

						<p className="text-xs mt-1">{t('embedding-select.empty.description')}</p>

					</div>

				) : (

					Object.entries(groups).map(([type, items], idx) => {

						const isSingle = items.length === 1;

						return (

							<DropdownMenuGroup key={type}>

								{idx > 0 && <DropdownMenuSeparator />}

								<DropdownMenuLabel>

									{type.replace(/_credential$/, '')}

								</DropdownMenuLabel>

								{isSingle

									? items[0].models.map((m) => (

											<DropdownMenuItem

												key={m.name}

												onSelect={() =>

													handleSelect(type, items[0].credential.id, m)

												}

											>

												{m.label}

											</DropdownMenuItem>

										))

									: items.map(({ credential, models }) => {

											const credName =

												(credential.data.name as string) ||

												credential.id.slice(0, 8);

											return (

												<DropdownMenuSub key={credential.id}>

													<DropdownMenuSubTrigger>

														{credName}

													</DropdownMenuSubTrigger>

													<DropdownMenuSubContent className="max-h-60 overflow-y-auto">

														{models.map((m) => (

															<DropdownMenuItem

																key={m.name}

																onSelect={() =>

																	handleSelect(

																		type,

																		credential.id,

																		m,

																	)

																}

															>

																{m.label}

															</DropdownMenuItem>

														))}

													</DropdownMenuSubContent>

												</DropdownMenuSub>

											);

										})}

							</DropdownMenuGroup>

						);

					})

				)}

				<DropdownMenuSeparator />

				<DropdownMenuItem onSelect={onAddCredential}>

					<PlusCircle className="size-4" />

					<span>{t('embedding-select.addCredential')}</span>

				</DropdownMenuItem>

			</DropdownMenuContent>

		</DropdownMenu>
	);
}