Files
goose/ui/desktop/src/components/welcome_screen/ProviderGrid.tsx
2025-01-30 13:38:45 -08:00

193 lines
6.0 KiB
TypeScript

import React from 'react';
import { Button } from '../ui/button';
import {
supported_providers,
required_keys,
provider_aliases,
} from '../settings/models/hardcoded_stuff';
import { useActiveKeys } from '../settings/api_keys/ActiveKeysContext';
import { ProviderSetupModal } from '../settings/ProviderSetupModal';
import { useModel } from '../settings/models/ModelContext';
import { useRecentModels } from '../settings/models/RecentModels';
import { createSelectedModel } from '../settings/models/utils';
import { getDefaultModel } from '../settings/models/hardcoded_stuff';
import { initializeSystem } from '../../utils/providerUtils';
import { getApiUrl, getSecretKey } from '../../config';
import { toast } from 'react-toastify';
import { getActiveProviders, isSecretKey } from '../settings/api_keys/utils';
import { useNavigate } from 'react-router-dom';
import { BaseProviderGrid, getProviderDescription } from '../settings/providers/BaseProviderGrid';
interface ProviderGridProps {
onSubmit?: () => void;
}
export function ProviderGrid({ onSubmit }: ProviderGridProps) {
const { activeKeys, setActiveKeys } = useActiveKeys();
const [selectedId, setSelectedId] = React.useState<string | null>(null);
const [showSetupModal, setShowSetupModal] = React.useState(false);
const { switchModel } = useModel();
const { addRecentModel } = useRecentModels();
const navigate = useNavigate();
const providers = React.useMemo(() => {
return supported_providers.map((providerName) => {
const alias =
provider_aliases.find((p) => p.provider === providerName)?.alias ||
providerName.toLowerCase();
const isConfigured = activeKeys.includes(providerName);
return {
id: alias,
name: providerName,
isConfigured,
description: getProviderDescription(providerName),
};
});
}, [activeKeys]);
const handleConfigure = async (provider) => {
const providerId = provider.id.toLowerCase();
const modelName = getDefaultModel(providerId);
const model = createSelectedModel(providerId, modelName);
await initializeSystem(providerId, model.name);
switchModel(model);
addRecentModel(model);
localStorage.setItem('GOOSE_PROVIDER', providerId);
toast.success(
`Selected ${provider.name} provider. Starting Goose with default model: ${getDefaultModel(provider.name.toLowerCase())}.`
);
onSubmit?.();
};
const handleAddKeys = (provider) => {
setSelectedId(provider.id);
setShowSetupModal(true);
};
const handleModalSubmit = async (apiKey: string) => {
if (!selectedId) return;
const provider = providers.find((p) => p.id === selectedId)?.name;
const keyName = required_keys[provider]?.[0];
if (!keyName) {
console.error(`No key found for provider ${provider}`);
return;
}
const isSecret = isSecretKey(keyName);
try {
if (selectedId && providers.find((p) => p.id === selectedId)?.isConfigured) {
const deleteResponse = await fetch(getApiUrl('/configs/delete'), {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
'X-Secret-Key': getSecretKey(),
},
body: JSON.stringify({
key: keyName,
isSecret,
}),
});
if (!deleteResponse.ok) {
const errorText = await deleteResponse.text();
console.error('Delete response error:', errorText);
throw new Error('Failed to delete old key');
}
}
const storeResponse = await fetch(getApiUrl('/configs/store'), {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Secret-Key': getSecretKey(),
},
body: JSON.stringify({
key: keyName,
value: apiKey.trim(),
isSecret,
}),
});
if (!storeResponse.ok) {
const errorText = await storeResponse.text();
console.error('Store response error:', errorText);
throw new Error('Failed to store new key');
}
const isUpdate = selectedId && providers.find((p) => p.id === selectedId)?.isConfigured;
const toastInfo = isSecret ? 'API key' : 'host';
toast.success(
isUpdate
? `Successfully updated ${toastInfo} for ${provider}`
: `Successfully added ${toastInfo} for ${provider}`
);
const updatedKeys = await getActiveProviders();
setActiveKeys(updatedKeys);
setShowSetupModal(false);
setSelectedId(null);
} catch (error) {
console.error('Error handling modal submit:', error);
toast.error(
`Failed to ${selectedId && providers.find((p) => p.id === selectedId)?.isConfigured ? 'update' : 'add'} API key for ${provider}`
);
}
};
const handleSelect = (providerId: string) => {
setSelectedId(selectedId === providerId ? null : providerId);
};
// Add useEffect for Esc key handling
React.useEffect(() => {
const handleEsc = (event: KeyboardEvent) => {
if (event.key === 'Escape') {
setSelectedId(null);
}
};
window.addEventListener('keydown', handleEsc);
return () => {
window.removeEventListener('keydown', handleEsc);
};
}, []);
return (
<div className="space-y-4 max-w-[1400px] mx-auto">
<BaseProviderGrid
providers={providers}
isSelectable={true}
selectedId={selectedId}
onSelect={handleSelect}
onAddKeys={handleAddKeys}
onTakeoff={(provider) => {
handleConfigure(provider);
}}
/>
{showSetupModal && selectedId && (
<div className="relative z-[9999]">
<ProviderSetupModal
provider={providers.find((p) => p.id === selectedId)?.name}
model="Example Model"
endpoint="Example Endpoint"
onSubmit={handleModalSubmit}
onCancel={() => {
setShowSetupModal(false);
setSelectedId(null);
}}
/>
</div>
)}
</div>
);
}