diff --git a/ui/desktop/src/components/settings/Keys.tsx b/ui/desktop/src/components/settings/Keys.tsx index a10f132e7..32ef19159 100644 --- a/ui/desktop/src/components/settings/Keys.tsx +++ b/ui/desktop/src/components/settings/Keys.tsx @@ -1,35 +1,33 @@ import React, { useEffect, useState } from 'react'; import { getApiUrl, getSecretKey } from "../../config"; -import { FaKey, FaExclamationCircle, FaPencilAlt, FaTrash, FaArrowLeft, FaPlus } from 'react-icons/fa'; +import { FaArrowLeft } from 'react-icons/fa'; import { showToast } from '../ui/toast'; import { useNavigate } from 'react-router-dom'; -import { - Modal, - ModalContent, - ModalHeader, - ModalTitle -} from '../ui/modal'; -import { initializeSystem } from '../../utils/providerUtils'; -import {getSecretsSettings, transformProviderSecretsResponse, transformSecrets} from './providers/utils' -import { SecretDetails, Provider, ProviderResponse } from './providers/types' -import { getStoredProvider } from "../../utils/providerUtils" -import { ProviderSetupModal } from "../welcome_screen/ProviderSetupModal" - +import { Modal, ModalContent, ModalHeader, ModalTitle } from '../ui/modal'; +import { initializeSystem, getStoredProvider } from '../../utils/providerUtils'; +import { + getSecretsSettings, + transformProviderSecretsResponse, + transformSecrets, +} from './providers/utils'; +import { ProviderSetupModal } from "../welcome_screen/ProviderSetupModal"; +import { Provider } from './providers/types' +import { ProviderCard } from './providers/ProviderCard' +import { ConfirmDeletionModal } from './modals/ConfirmDeletionModal' + + +// Main Component: Keys export default function Keys() { const navigate = useNavigate(); - const [secrets, setSecrets] = useState([]); - const [expandedProviders, setExpandedProviders] = useState>(new Set()); - const [providers, setProviders] = useState([]); - const [showTestModal, setShowTestModal] = useState(false); - const [currentKey, setCurrentKey] = useState(null); // Tracks key being edited/added - const [selectedProvider, setSelectedProvider] = useState(null); - const [showSetProviderKeyModal, setShowSetProviderKeyModal] = useState(false) - const [keyToDelete, setKeyToDelete] = useState<{providerId: string, key: string} | null>(null); + const [secrets, setSecrets] = useState([]); + const [expandedProviders, setExpandedProviders] = useState(new Set()); + const [providers, setProviders] = useState([]); + const [showSetProviderKeyModal, setShowSetProviderKeyModal] = useState(false); + const [currentKey, setCurrentKey] = useState(null); + const [selectedProvider, setSelectedProvider] = useState(null); const [isChangingProvider, setIsChangingProvider] = useState(false); + const [keyToDelete, setKeyToDelete] = useState<{providerId: string, key: string} | null>(null); - useEffect(() => { - console.log("Modal visibility:", showSetProviderKeyModal); - }, [showSetProviderKeyModal]); useEffect(() => { const fetchSecrets = async () => { @@ -45,15 +43,16 @@ export default function Keys() { // ] const transformedSecrets = transformSecrets(data) console.log("transformedSecrets", transformedSecrets) - + setSecrets(transformedSecrets); - + // Check and expand active provider + // TODO: fix the below lint error const config = window.electron.getConfig(); const gooseProvider = getStoredProvider(config); if (gooseProvider) { - const matchedProvider = transformedProviders.find(provider => - provider.id.toLowerCase() === gooseProvider + const matchedProvider = transformedProviders.find(provider => + provider.id.toLowerCase() === gooseProvider ); if (matchedProvider) { setExpandedProviders(new Set([matchedProvider.id])); @@ -69,10 +68,20 @@ export default function Keys() { fetchSecrets(); }, []); + const toggleProvider = (providerId) => { + setExpandedProviders(prev => { + const newSet = new Set(prev); + if (newSet.has(providerId)) { + newSet.delete(providerId); + } else { + newSet.add(providerId); + } + return newSet; + }); + }; + const getProviderStatus = (provider: Provider) => { - const providerSecrets = provider.keys.map(key => - secrets.find(s => s.key === key) - ); + const providerSecrets = provider.keys.map(key => secrets.find(s => s.key === key)); return providerSecrets.some(s => !s?.is_set); }; @@ -155,7 +164,7 @@ export default function Keys() { const handleDeleteKey = async (providerId: string, key: string) => { // Find the secret to check its source const secret = secrets.find(s => s.key === key); - + if (secret?.location === 'env') { showToast("This key is set in your environment. Please remove it from your ~/.zshrc or equivalent file.", "error"); return; @@ -197,239 +206,77 @@ export default function Keys() { } }; - const handleDeleteProvider = (providerId: string) => { - setProviders(providers.filter(p => p.id !== providerId)); - showToast(`Provider ${providerId} removed`, "success"); - }; - - const toggleProvider = (providerId: string) => { - setExpandedProviders(prev => { - const newSet = new Set(prev); - if (newSet.has(providerId)) { - newSet.delete(providerId); - } else { - newSet.add(providerId); - } - return newSet; - }); - }; - const isProviderSupported = (providerId: string) => { const provider = providers.find(p => p.id === providerId); return provider?.supported ?? false; }; - const handleTestProviders = async () => { - try { - const response = await fetch(getApiUrl("/secrets/providers"), { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'X-Secret-Key': getSecretKey(), - }, - body: JSON.stringify({ - providers: ["OpenAI", "Anthropic", "MyProvider"] - }) - }); - - if (!response.ok) { - throw new Error('Failed to fetch provider status'); - } - - const data = await response.json() as Record; - setShowTestModal(true); - } catch (error) { - console.error('Error testing providers:', error); - showToast("Failed to test providers", "error"); - } - }; - - const handleSelectProvider = async (providerId: string) => { + const handleSelectProvider = async (providerId) => { setIsChangingProvider(true); try { - // Update localStorage + // Update localStorage + // TODO: do we need to consider cases where GOOSE_PROVIDER is set in the zshrc file? const provider = providers.find(p => p.id === providerId); if (provider) { localStorage.setItem("GOOSE_PROVIDER", provider.name); - initializeSystem(provider.id) + initializeSystem(provider.id); showToast(`Switched to ${provider.name}`, "success"); } } catch (error) { - console.error("Failed to change provider:", error); - showToast(error instanceof Error ? error.message : "Failed to change provider", "error"); + showToast("Failed to change provider", "error"); } finally { setIsChangingProvider(false); } }; return ( -
-
- -

Providers

-
- - +

Providers

-
-
- {providers.map((provider) => { - const hasUnsetKeys = getProviderStatus(provider); - const isExpanded = expandedProviders.has(provider.id); - const isSupported = isProviderSupported(provider.id); - - return ( -
-
- -
- - {isSupported && isExpanded && ( -
- {provider.keys.map(key => { - const secret = secrets.find(s => s.key === key); - return ( -
-
-

{key}

-

- Source: {secret?.location || 'none'} -

-
-
- - {secret?.is_set ? 'Key set' : 'Missing'} - - - -
-
- ); - })} - - {provider.id.toLowerCase() !== localStorage.getItem("GOOSE_PROVIDER")?.toLowerCase() && ( - - )} -
- )} -
- ); - })} -
+
+ {providers.map((provider) => ( + + ))} +
- {showSetProviderKeyModal && currentKey && selectedProvider && ( - handleSubmit(apiKey)} // Call handleSubmit when submitting - onCancel={() => setShowSetProviderKeyModal(false)} // Close modal on cancel - /> - )} - - - setKeyToDelete(null)}> - - - Confirm Deletion - -
-

- Are you sure you want to delete this API key from the keychain? -

-
- - -
-
-
-
-
+ {showSetProviderKeyModal && currentKey && selectedProvider && ( + handleSubmit(apiKey)} // Call handleSubmit when submitting + onCancel={() => setShowSetProviderKeyModal(false)} // Close modal on cancel + /> + )} + + {keyToDelete && ( + setKeyToDelete(null)} + onConfirm={confirmDelete} + /> + )} + ); -} \ No newline at end of file +} diff --git a/ui/desktop/src/components/settings/modals/ConfirmDeletionModal.tsx b/ui/desktop/src/components/settings/modals/ConfirmDeletionModal.tsx new file mode 100644 index 000000000..9eeea7533 --- /dev/null +++ b/ui/desktop/src/components/settings/modals/ConfirmDeletionModal.tsx @@ -0,0 +1,33 @@ +import React from 'react'; +import { Modal, ModalContent, ModalHeader, ModalTitle } from '../../ui/modal'; + +export const ConfirmDeletionModal = ({ keyToDelete, onCancel, onConfirm }) => { + return ( + + + + Confirm Deletion + +
+

+ Are you sure you want to delete this API key from the keychain? +

+
+ + +
+
+
+
+ ); +}; diff --git a/ui/desktop/src/components/settings/providers/ProviderCard.tsx b/ui/desktop/src/components/settings/providers/ProviderCard.tsx index 138d90b9d..503425924 100644 --- a/ui/desktop/src/components/settings/providers/ProviderCard.tsx +++ b/ui/desktop/src/components/settings/providers/ProviderCard.tsx @@ -1,47 +1,22 @@ -import React, { useEffect, useState } from 'react'; -import { getApiUrl, getSecretKey } from "../../../config"; -import { FaKey, FaExclamationCircle, FaPencilAlt, FaTrash, FaArrowLeft, FaPlus } from 'react-icons/fa'; +import { FaKey, FaExclamationCircle, FaPencilAlt, FaTrash, FaPlus } from 'react-icons/fa'; +import React from "react"; -import { - Modal, - ModalContent, - ModalHeader, - ModalTitle -} from '../../ui/modal'; -import { initializeSystem } from '../../../utils/providerUtils'; -import {getSecretsSettings, transformProviderSecretsResponse, transformSecrets} from './utils' -import { SecretDetails, Provider, ProviderResponse } from './types' - -function ProviderCard({ +export const ProviderCard = ({ provider, secrets, - expandedProviders, + isExpanded, + isSupported, + isChangingProvider, toggleProvider, handleAddOrEditKey, handleDeleteKey, handleSelectProvider, - isChangingProvider, getProviderStatus, - isProviderSupported, - }: { - provider: Provider; - secrets: SecretDetails[]; - expandedProviders: Set; - toggleProvider: (id: string) => void; - handleAddOrEditKey: (key: string, providerName: string) => void; - handleDeleteKey: (providerId: string, key: string) => void; - handleSelectProvider: (providerId: string) => void; - isChangingProvider: boolean; - getProviderStatus: (provider: Provider) => boolean; - isProviderSupported: (providerId: string) => boolean; -}) { + }) => { const hasUnsetKeys = getProviderStatus(provider); - const isExpanded = expandedProviders.has(provider.id); - const isSupported = isProviderSupported(provider.id); return (
- {/* Provider Header */}
-

{provider.name}

- {!isSupported && ( - - Not Supported - - )} +
+

{provider.name}

+ {provider.id.toLowerCase() === localStorage.getItem("GOOSE_PROVIDER")?.toLowerCase() && ( + + Selected Provider + + )} + {!isSupported && ( + + Not Supported + + )} +
+

+ {isSupported ? provider.description : 'Provider not supported'} +

- {isSupported && hasUnsetKeys && } + {isSupported && hasUnsetKeys && ( + + )} - {/* Provider Keys */} {isSupported && isExpanded && (
- {provider.keys.map((key) => { - const secret = secrets.find((s) => s.key === key); + {provider.keys.map(key => { + const secret = secrets.find(s => s.key === key); return (
@@ -76,18 +62,17 @@ function ProviderCard({

Source: {secret?.location || 'none'}

- + {secret?.is_set ? 'Key set' : 'Missing'} @@ -98,7 +83,13 @@ function ProviderCard({ ? 'text-red-500 hover:text-red-700 dark:text-red-400 dark:hover:text-red-200 hover:bg-red-100 dark:hover:bg-red-900' : 'text-gray-300 dark:text-gray-600 cursor-not-allowed' }`} + title={ + secret?.is_set + ? "Delete key from keychain" + : "No key to delete - Add a key first before deleting" + } disabled={!secret?.is_set} + aria-disabled={!secret?.is_set} > @@ -106,6 +97,7 @@ function ProviderCard({
); })} + {provider.id.toLowerCase() !== localStorage.getItem("GOOSE_PROVIDER")?.toLowerCase() && (
); -} +};