Skip to content
Merged
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
43 changes: 40 additions & 3 deletions ui/desktop/src/components/settings/providers/ProviderGrid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { Plus } from 'lucide-react';
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '../../ui/dialog';
import CustomProviderForm from './modal/subcomponents/forms/CustomProviderForm';
import { SwitchModelModal } from '../models/subcomponents/SwitchModelModal';
import { useModelAndProvider } from '../../ModelAndProviderContext';
import type { View } from '../../../utils/navigationUtils';

const GridLayout = memo(function GridLayout({ children }: { children: React.ReactNode }) {
Expand Down Expand Up @@ -65,10 +66,13 @@ function ProviderCards({
const [showCustomProviderModal, setShowCustomProviderModal] = useState(false);
const [showSwitchModelModal, setShowSwitchModelModal] = useState(false);
const [switchModelProvider, setSwitchModelProvider] = useState<string | null>(null);
const [isActiveProvider, setIsActiveProvider] = useState(false);
const { getCurrentModelAndProvider } = useModelAndProvider();
const [editingProvider, setEditingProvider] = useState<{
id: string;
config: DeclarativeProviderConfig;
isEditable: boolean;
providerType: string;
} | null>(null);

const handleProviderLaunchWithModelSelection = useCallback((provider: ProviderDetails) => {
Expand All @@ -92,14 +96,24 @@ function ProviderCards({
id: provider.name,
config: result.data.config,
isEditable: result.data.is_editable,
providerType: provider.provider_type,
});

// Check if this is the active provider
try {
const providerModel = await getCurrentModelAndProvider();
setIsActiveProvider(provider.name === providerModel.provider);
} catch {
setIsActiveProvider(false);
}

setShowCustomProviderModal(true);
}
} else {
openModal(provider);
}
},
[openModal]
[openModal, getCurrentModelAndProvider]
);

const handleUpdateCustomProvider = useCallback(
Expand All @@ -124,9 +138,26 @@ function ProviderCards({
[editingProvider, refreshProviders]
);

const handleDeleteCustomProvider = useCallback(async () => {
if (!editingProvider) return;

const { removeCustomProvider } = await import('../../../api');
await removeCustomProvider({
path: { id: editingProvider.id },
throwOnError: true,
});
setShowCustomProviderModal(false);
setEditingProvider(null);
setIsActiveProvider(false);
if (refreshProviders) {
refreshProviders();
}
}, [editingProvider, refreshProviders]);

const handleCloseModal = useCallback(() => {
setShowCustomProviderModal(false);
setEditingProvider(null);
setIsActiveProvider(false);
}, []);

const onCloseProviderConfig = useCallback(() => {
Expand Down Expand Up @@ -178,8 +209,10 @@ function ProviderCards({
const providerCards = useMemo(() => {
// providers needs to be an array
const providersArray = Array.isArray(providers) ? providers : [];
// Sort providers alphabetically by name
const sortedProviders = [...providersArray].sort((a, b) => a.name.localeCompare(b.name));
// Sort providers alphabetically by display name
const sortedProviders = [...providersArray].sort((a, b) =>
a.metadata.display_name.localeCompare(b.metadata.display_name)
);
const cards = sortedProviders.map((provider) => (
<ProviderCard
key={provider.name}
Expand Down Expand Up @@ -222,6 +255,10 @@ function ProviderCards({
isEditable={editable}
onSubmit={editingProvider ? handleUpdateCustomProvider : handleCreateCustomProvider}
onCancel={handleCloseModal}
onDelete={
editingProvider?.providerType === 'Custom' ? handleDeleteCustomProvider : undefined
}
isActiveProvider={isActiveProvider}
/>
</DialogContent>
</Dialog>{' '}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,22 @@ import { Select } from '../../../../../ui/Select';
import { Button } from '../../../../../ui/button';
import { SecureStorageNotice } from '../SecureStorageNotice';
import { UpdateCustomProviderRequest } from '../../../../../../api';
import { Trash2, AlertTriangle } from 'lucide-react';

interface CustomProviderFormProps {
onSubmit: (data: UpdateCustomProviderRequest) => void;
onCancel: () => void;
onDelete?: () => Promise<void>;
isActiveProvider?: boolean;
initialData: UpdateCustomProviderRequest | null;
isEditable?: boolean;
}

export default function CustomProviderForm({
onSubmit,
onCancel,
onDelete,
isActiveProvider = false,
initialData,
isEditable,
}: CustomProviderFormProps) {
Expand All @@ -26,6 +31,7 @@ export default function CustomProviderForm({
const [requiresApiKey, setRequiresApiKey] = useState(false);
const [supportsStreaming, setSupportsStreaming] = useState(true);
const [validationErrors, setValidationErrors] = useState<Record<string, string>>({});
const [showDeleteConfirmation, setShowDeleteConfirmation] = useState(false);

useEffect(() => {
if (initialData) {
Expand Down Expand Up @@ -257,12 +263,62 @@ export default function CustomProviderForm({
</>
)}
<SecureStorageNotice />
<div className="flex justify-end space-x-2 pt-4">
<Button type="button" variant="outline" onClick={onCancel}>
Cancel
</Button>
<Button type="submit">{initialData ? 'Update Provider' : 'Create Provider'}</Button>
</div>

{showDeleteConfirmation ? (
<div className="pt-4 space-y-3">
{isActiveProvider ? (
<div className="px-4 py-3 bg-yellow-600/20 border border-yellow-500/30 rounded">
<p className="text-yellow-500 text-sm flex items-start">
<AlertTriangle className="h-4 w-4 mr-2 mt-0.5 flex-shrink-0" />
<span>
You cannot delete this provider while it's currently in use. Please switch to a
different model first.
</span>
</p>
</div>
) : (
<div className="px-4 py-3 bg-red-900/20 border border-red-500/30 rounded">
<p className="text-red-400 text-sm">
Are you sure you want to delete this custom provider? This will permanently remove
the provider and its stored API key. This action cannot be undone.
</p>
</div>
)}
<div className="flex justify-end space-x-2">
<Button
type="button"
variant="outline"
onClick={() => setShowDeleteConfirmation(false)}
>
Cancel
</Button>
{!isActiveProvider && (
<Button type="button" variant="destructive" onClick={onDelete}>
<Trash2 className="h-4 w-4 mr-2" />
Confirm Delete
</Button>
)}
</div>
</div>
) : (
<div className="flex justify-end space-x-2 pt-4">
{initialData && onDelete && (
<Button
type="button"
variant="outline"
className="text-red-500 hover:text-red-600 mr-auto"
onClick={() => setShowDeleteConfirmation(true)}
>
<Trash2 className="h-4 w-4 mr-2" />
Delete Provider
</Button>
)}
<Button type="button" variant="outline" onClick={onCancel}>
Cancel
</Button>
<Button type="submit">{initialData ? 'Update Provider' : 'Create Provider'}</Button>
</div>
)}
</form>
);
}
Loading