Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 40 additions & 6 deletions ui/src/app/models/new/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@ function ModelPageContent() {
const [errors, setErrors] = useState<ValidationErrors>({});
const [isApiKeyNeeded, setIsApiKeyNeeded] = useState(true);
const [isParamsSectionExpanded, setIsParamsSectionExpanded] = useState(false);
const [isFetchingModels, setIsFetchingModels] = useState(false);
const isOllamaSelected = selectedProvider?.type === "Ollama";

useEffect(() => {
Expand Down Expand Up @@ -259,7 +260,14 @@ function ModelPageContent() {
useEffect(() => {
if (selectedProvider) {
const requiredKeys = selectedProvider.requiredParams || [];
const optionalKeys = selectedProvider.optionalParams || [];
let optionalKeys = [...(selectedProvider.optionalParams || [])];

// Add baseUrl to optional params for providers that support it
const providersWithBaseUrl = ['OpenAI', 'Anthropic', 'Gemini'];
if (providersWithBaseUrl.includes(selectedProvider.type) && !optionalKeys.includes('baseUrl')) {
optionalKeys = ['baseUrl', ...optionalKeys];
}

const currentModelRequiresReset = !isEditMode;

if (currentModelRequiresReset) {
Expand Down Expand Up @@ -315,6 +323,16 @@ function ModelPageContent() {
}
}, [isApiKeyNeeded, errors.apiKey]);

// Auto-select OpenAI provider on page load (create mode only)
useEffect(() => {
if (!isEditMode && providers.length > 0 && !selectedProvider) {
const openAIProvider = providers.find(p => p.type === 'OpenAI');
if (openAIProvider) {
setSelectedProvider(openAIProvider);
}
}
}, [isEditMode, providers, selectedProvider]);

const validateForm = () => {
const newErrors: ValidationErrors = { requiredParams: {} };

Expand Down Expand Up @@ -384,6 +402,25 @@ function ModelPageContent() {
}
};

const handleFetchModels = async () => {
setIsFetchingModels(true);
try {
const response = await getModels();

if (response.error || !response.data) {
throw new Error(response.error || "Failed to fetch models");
}

setProviderModelsData(response.data);
toast.success("Models refreshed successfully");
} catch (error) {
const errorMessage = error instanceof Error ? error.message : "Failed to fetch models";
toast.error(errorMessage);
} finally {
setIsFetchingModels(false);
}
};

const handleSubmit = async () => {
if (!selectedCombinedModel) {
setErrors(prev => ({...prev, selectedCombinedModel: "Provider and Model selection is required"}));
Expand Down Expand Up @@ -545,6 +582,8 @@ function ModelPageContent() {
isEditMode={isEditMode}
modelTag={modelTag}
onModelTagChange={setModelTag}
onFetchModels={handleFetchModels}
isFetchingModels={isFetchingModels}
/>

<AuthSection
Expand Down Expand Up @@ -610,8 +649,3 @@ export default function ModelPage() {
</React.Suspense>
);
}





91 changes: 91 additions & 0 deletions ui/src/components/ModelCombobox.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import React, { useState, useMemo } from 'react';
import { Button } from '@/components/ui/button';
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
import { Command, CommandEmpty, CommandInput, CommandItem, CommandList } from "@/components/ui/command";
import { Check, ChevronsUpDown } from 'lucide-react';
import { cn } from '@/lib/utils';
import { ProviderModel } from '@/types';

interface ModelComboboxProps {
models: ProviderModel[];
value: string | undefined;
onChange: (modelName: string, functionCalling: boolean) => void;
disabled?: boolean;
placeholder?: string;
emptyMessage?: string;
}

export function ModelCombobox({
models,
value,
onChange,
disabled = false,
placeholder = "Select model...",
emptyMessage = "No models available"
}: ModelComboboxProps) {
const [open, setOpen] = useState(false);

const sortedModels = useMemo(() => {
return [...models].sort((a, b) => a.name.localeCompare(b.name));
}, [models]);

const selectedModel = useMemo(() => {
return sortedModels.find(m => m.name === value);
}, [sortedModels, value]);

const triggerContent = useMemo(() => {
if (selectedModel) {
return selectedModel.name;
}
if (sortedModels.length === 0 && !disabled) return emptyMessage;
return placeholder;
}, [selectedModel, sortedModels.length, disabled, emptyMessage, placeholder]);

return (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
aria-expanded={open}
className={cn(
"w-full justify-between",
!value && "text-muted-foreground"
)}
disabled={disabled || sortedModels.length === 0}
>
<span className="flex items-center truncate">
{triggerContent}
</span>
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-[--radix-popover-trigger-width] p-0">
<Command>
<CommandInput placeholder="Search models..." />
<CommandList>
<CommandEmpty>No model found.</CommandEmpty>
{sortedModels.map((model) => (
<CommandItem
key={model.name}
value={model.name}
onSelect={() => {
onChange(model.name, model.function_calling ?? false);
setOpen(false);
}}
>
<Check
className={cn(
"mr-2 h-4 w-4",
value === model.name ? "opacity-100" : "opacity-0"
)}
/>
{model.name}
</CommandItem>
))}
</CommandList>
</Command>
</PopoverContent>
</Popover>
);
}
118 changes: 118 additions & 0 deletions ui/src/components/ProviderCombobox.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import React, { useState, useMemo, useCallback } from 'react';
import { Button } from '@/components/ui/button';
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@/components/ui/command";
import { Check, ChevronsUpDown } from 'lucide-react';
import { cn } from '@/lib/utils';
import { Provider } from '@/types';
import { ModelProviderKey } from '@/lib/providers';
import { OpenAI } from './icons/OpenAI';
import { Anthropic } from './icons/Anthropic';
import { Ollama } from './icons/Ollama';
import { Azure } from './icons/Azure';
import { Gemini } from './icons/Gemini';

interface ProviderComboboxProps {
providers: Provider[];
value: Provider | null;
onChange: (provider: Provider) => void;
disabled?: boolean;
loading?: boolean;
}

export function ProviderCombobox({
providers,
value,
onChange,
disabled = false,
loading = false,
}: ProviderComboboxProps) {
const [open, setOpen] = useState(false);

const getProviderIcon = useCallback((providerType: string | undefined): React.ReactNode | null => {
const PROVIDER_ICONS: Record<ModelProviderKey, React.ComponentType<{ className?: string }>> = {
'OpenAI': OpenAI,
'Anthropic': Anthropic,
'Ollama': Ollama,
'AzureOpenAI': Azure,
'Gemini': Gemini,
'GeminiVertexAI': Gemini,
'AnthropicVertexAI': Anthropic,
};

if (!providerType || !(providerType in PROVIDER_ICONS)) {
return null;
}
const IconComponent = PROVIDER_ICONS[providerType as ModelProviderKey];
return <IconComponent className="h-4 w-4 mr-2 shrink-0" />;
}, []);

const sortedProviders = useMemo(() => {
return [...providers].sort((a, b) => a.name.localeCompare(b.name));
}, [providers]);

const triggerContent = useMemo(() => {
if (loading) return "Loading providers...";
if (value) {
return (
<>
{getProviderIcon(value.type)}
{value.name}
</>
);
}
if (sortedProviders.length === 0) return "No providers available";
return "Select provider...";
}, [loading, value, sortedProviders.length, getProviderIcon]);

return (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
aria-expanded={open}
className={cn(
"w-full justify-between",
!value && !loading && "text-muted-foreground"
)}
disabled={disabled || loading || sortedProviders.length === 0}
>
<span className="flex items-center truncate">
{triggerContent}
</span>
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-[--radix-popover-trigger-width] p-0">
<Command>
<CommandInput placeholder="Search providers..." />
<CommandList>
<CommandEmpty>No provider found.</CommandEmpty>
<CommandGroup>
{sortedProviders.map((provider) => (
<CommandItem
key={provider.type}
value={provider.name}
onSelect={() => {
onChange(provider);
setOpen(false);
}}
>
<Check
className={cn(
"mr-2 h-4 w-4",
value?.type === provider.type ? "opacity-100" : "opacity-0"
)}
/>
{getProviderIcon(provider.type)}
{provider.name}
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
);
}
Loading