Skip to content

Commit 83cc06d

Browse files
committed
split provider and Models in UI
Signed-off-by: Nagarjun Krishnan <[email protected]>
1 parent cfeb177 commit 83cc06d

File tree

4 files changed

+342
-29
lines changed

4 files changed

+342
-29
lines changed

ui/src/app/models/new/page.tsx

Lines changed: 40 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,7 @@ function ModelPageContent() {
128128
const [errors, setErrors] = useState<ValidationErrors>({});
129129
const [isApiKeyNeeded, setIsApiKeyNeeded] = useState(true);
130130
const [isParamsSectionExpanded, setIsParamsSectionExpanded] = useState(false);
131+
const [isFetchingModels, setIsFetchingModels] = useState(false);
131132
const isOllamaSelected = selectedProvider?.type === "Ollama";
132133

133134
useEffect(() => {
@@ -259,7 +260,14 @@ function ModelPageContent() {
259260
useEffect(() => {
260261
if (selectedProvider) {
261262
const requiredKeys = selectedProvider.requiredParams || [];
262-
const optionalKeys = selectedProvider.optionalParams || [];
263+
let optionalKeys = [...(selectedProvider.optionalParams || [])];
264+
265+
// Add baseUrl to optional params for providers that support it
266+
const providersWithBaseUrl = ['OpenAI', 'Anthropic', 'Gemini'];
267+
if (providersWithBaseUrl.includes(selectedProvider.type) && !optionalKeys.includes('baseUrl')) {
268+
optionalKeys = ['baseUrl', ...optionalKeys];
269+
}
270+
263271
const currentModelRequiresReset = !isEditMode;
264272

265273
if (currentModelRequiresReset) {
@@ -315,6 +323,16 @@ function ModelPageContent() {
315323
}
316324
}, [isApiKeyNeeded, errors.apiKey]);
317325

326+
// Auto-select OpenAI provider on page load (create mode only)
327+
useEffect(() => {
328+
if (!isEditMode && providers.length > 0 && !selectedProvider) {
329+
const openAIProvider = providers.find(p => p.type === 'OpenAI');
330+
if (openAIProvider) {
331+
setSelectedProvider(openAIProvider);
332+
}
333+
}
334+
}, [isEditMode, providers, selectedProvider]);
335+
318336
const validateForm = () => {
319337
const newErrors: ValidationErrors = { requiredParams: {} };
320338

@@ -384,6 +402,25 @@ function ModelPageContent() {
384402
}
385403
};
386404

405+
const handleFetchModels = async () => {
406+
setIsFetchingModels(true);
407+
try {
408+
const response = await getModels();
409+
410+
if (response.error || !response.data) {
411+
throw new Error(response.error || "Failed to fetch models");
412+
}
413+
414+
setProviderModelsData(response.data);
415+
toast.success("Models refreshed successfully");
416+
} catch (error) {
417+
const errorMessage = error instanceof Error ? error.message : "Failed to fetch models";
418+
toast.error(errorMessage);
419+
} finally {
420+
setIsFetchingModels(false);
421+
}
422+
};
423+
387424
const handleSubmit = async () => {
388425
if (!selectedCombinedModel) {
389426
setErrors(prev => ({...prev, selectedCombinedModel: "Provider and Model selection is required"}));
@@ -545,6 +582,8 @@ function ModelPageContent() {
545582
isEditMode={isEditMode}
546583
modelTag={modelTag}
547584
onModelTagChange={setModelTag}
585+
onFetchModels={handleFetchModels}
586+
isFetchingModels={isFetchingModels}
548587
/>
549588

550589
<AuthSection
@@ -610,8 +649,3 @@ export default function ModelPage() {
610649
</React.Suspense>
611650
);
612651
}
613-
614-
615-
616-
617-
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import React, { useState, useMemo } from 'react';
2+
import { Button } from '@/components/ui/button';
3+
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
4+
import { Command, CommandEmpty, CommandInput, CommandItem, CommandList } from "@/components/ui/command";
5+
import { Check, ChevronsUpDown } from 'lucide-react';
6+
import { cn } from '@/lib/utils';
7+
import { ProviderModel } from '@/types';
8+
9+
interface ModelComboboxProps {
10+
models: ProviderModel[];
11+
value: string | undefined;
12+
onChange: (modelName: string, functionCalling: boolean) => void;
13+
disabled?: boolean;
14+
placeholder?: string;
15+
emptyMessage?: string;
16+
}
17+
18+
export function ModelCombobox({
19+
models,
20+
value,
21+
onChange,
22+
disabled = false,
23+
placeholder = "Select model...",
24+
emptyMessage = "No models available"
25+
}: ModelComboboxProps) {
26+
const [open, setOpen] = useState(false);
27+
28+
const sortedModels = useMemo(() => {
29+
return [...models].sort((a, b) => a.name.localeCompare(b.name));
30+
}, [models]);
31+
32+
const selectedModel = useMemo(() => {
33+
return sortedModels.find(m => m.name === value);
34+
}, [sortedModels, value]);
35+
36+
const triggerContent = useMemo(() => {
37+
if (selectedModel) {
38+
return selectedModel.name;
39+
}
40+
if (sortedModels.length === 0 && !disabled) return emptyMessage;
41+
return placeholder;
42+
}, [selectedModel, sortedModels.length, disabled, emptyMessage, placeholder]);
43+
44+
return (
45+
<Popover open={open} onOpenChange={setOpen}>
46+
<PopoverTrigger asChild>
47+
<Button
48+
variant="outline"
49+
role="combobox"
50+
aria-expanded={open}
51+
className={cn(
52+
"w-full justify-between",
53+
!value && "text-muted-foreground"
54+
)}
55+
disabled={disabled || sortedModels.length === 0}
56+
>
57+
<span className="flex items-center truncate">
58+
{triggerContent}
59+
</span>
60+
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
61+
</Button>
62+
</PopoverTrigger>
63+
<PopoverContent className="w-[--radix-popover-trigger-width] p-0">
64+
<Command>
65+
<CommandInput placeholder="Search models..." />
66+
<CommandList>
67+
<CommandEmpty>No model found.</CommandEmpty>
68+
{sortedModels.map((model) => (
69+
<CommandItem
70+
key={model.name}
71+
value={model.name}
72+
onSelect={() => {
73+
onChange(model.name, model.function_calling ?? false);
74+
setOpen(false);
75+
}}
76+
>
77+
<Check
78+
className={cn(
79+
"mr-2 h-4 w-4",
80+
value === model.name ? "opacity-100" : "opacity-0"
81+
)}
82+
/>
83+
{model.name}
84+
</CommandItem>
85+
))}
86+
</CommandList>
87+
</Command>
88+
</PopoverContent>
89+
</Popover>
90+
);
91+
}
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
import React, { useState, useMemo, useCallback } from 'react';
2+
import { Button } from '@/components/ui/button';
3+
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
4+
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@/components/ui/command";
5+
import { Check, ChevronsUpDown } from 'lucide-react';
6+
import { cn } from '@/lib/utils';
7+
import { Provider } from '@/types';
8+
import { ModelProviderKey } from '@/lib/providers';
9+
import { OpenAI } from './icons/OpenAI';
10+
import { Anthropic } from './icons/Anthropic';
11+
import { Ollama } from './icons/Ollama';
12+
import { Azure } from './icons/Azure';
13+
import { Gemini } from './icons/Gemini';
14+
15+
interface ProviderComboboxProps {
16+
providers: Provider[];
17+
value: Provider | null;
18+
onChange: (provider: Provider) => void;
19+
disabled?: boolean;
20+
loading?: boolean;
21+
}
22+
23+
export function ProviderCombobox({
24+
providers,
25+
value,
26+
onChange,
27+
disabled = false,
28+
loading = false,
29+
}: ProviderComboboxProps) {
30+
const [open, setOpen] = useState(false);
31+
32+
const getProviderIcon = useCallback((providerType: string | undefined): React.ReactNode | null => {
33+
const PROVIDER_ICONS: Record<ModelProviderKey, React.ComponentType<{ className?: string }>> = {
34+
'OpenAI': OpenAI,
35+
'Anthropic': Anthropic,
36+
'Ollama': Ollama,
37+
'AzureOpenAI': Azure,
38+
'Gemini': Gemini,
39+
'GeminiVertexAI': Gemini,
40+
'AnthropicVertexAI': Anthropic,
41+
};
42+
43+
if (!providerType || !(providerType in PROVIDER_ICONS)) {
44+
return null;
45+
}
46+
const IconComponent = PROVIDER_ICONS[providerType as ModelProviderKey];
47+
return <IconComponent className="h-4 w-4 mr-2 shrink-0" />;
48+
}, []);
49+
50+
const sortedProviders = useMemo(() => {
51+
return [...providers].sort((a, b) => a.name.localeCompare(b.name));
52+
}, [providers]);
53+
54+
const triggerContent = useMemo(() => {
55+
if (loading) return "Loading providers...";
56+
if (value) {
57+
return (
58+
<>
59+
{getProviderIcon(value.type)}
60+
{value.name}
61+
</>
62+
);
63+
}
64+
if (sortedProviders.length === 0) return "No providers available";
65+
return "Select provider...";
66+
}, [loading, value, sortedProviders.length, getProviderIcon]);
67+
68+
return (
69+
<Popover open={open} onOpenChange={setOpen}>
70+
<PopoverTrigger asChild>
71+
<Button
72+
variant="outline"
73+
role="combobox"
74+
aria-expanded={open}
75+
className={cn(
76+
"w-full justify-between",
77+
!value && !loading && "text-muted-foreground"
78+
)}
79+
disabled={disabled || loading || sortedProviders.length === 0}
80+
>
81+
<span className="flex items-center truncate">
82+
{triggerContent}
83+
</span>
84+
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
85+
</Button>
86+
</PopoverTrigger>
87+
<PopoverContent className="w-[--radix-popover-trigger-width] p-0">
88+
<Command>
89+
<CommandInput placeholder="Search providers..." />
90+
<CommandList>
91+
<CommandEmpty>No provider found.</CommandEmpty>
92+
<CommandGroup>
93+
{sortedProviders.map((provider) => (
94+
<CommandItem
95+
key={provider.type}
96+
value={provider.name}
97+
onSelect={() => {
98+
onChange(provider);
99+
setOpen(false);
100+
}}
101+
>
102+
<Check
103+
className={cn(
104+
"mr-2 h-4 w-4",
105+
value?.type === provider.type ? "opacity-100" : "opacity-0"
106+
)}
107+
/>
108+
{getProviderIcon(provider.type)}
109+
{provider.name}
110+
</CommandItem>
111+
))}
112+
</CommandGroup>
113+
</CommandList>
114+
</Command>
115+
</PopoverContent>
116+
</Popover>
117+
);
118+
}

0 commit comments

Comments
 (0)