diff --git a/app/components/chat/APIKeyManager.tsx b/app/components/chat/APIKeyManager.tsx index d40204867..5acbbf12a 100644 --- a/app/components/chat/APIKeyManager.tsx +++ b/app/components/chat/APIKeyManager.tsx @@ -1,7 +1,8 @@ -import React, { useState } from 'react'; +import React, { useState, useEffect, useCallback } from 'react'; import { IconButton } from '~/components/ui/IconButton'; import type { ProviderInfo } from '~/types/model'; import Cookies from 'js-cookie'; +import { providerBaseUrlEnvKeys } from '~/utils/constants'; interface APIKeyManagerProps { provider: ProviderInfo; @@ -11,11 +12,14 @@ interface APIKeyManagerProps { labelForGetApiKey?: string; } +// cache which stores whether the provider's API key is set via environment variable +const providerEnvKeyStatusCache: Record = {}; + const apiKeyMemoizeCache: { [k: string]: Record } = {}; export function getApiKeysFromCookies() { const storedApiKeys = Cookies.get('apiKeys'); - let parsedKeys = {}; + let parsedKeys: Record = {}; if (storedApiKeys) { parsedKeys = apiKeyMemoizeCache[storedApiKeys]; @@ -32,54 +36,137 @@ export function getApiKeysFromCookies() { export const APIKeyManager: React.FC = ({ provider, apiKey, setApiKey }) => { const [isEditing, setIsEditing] = useState(false); const [tempKey, setTempKey] = useState(apiKey); + const [isEnvKeySet, setIsEnvKeySet] = useState(false); + + // Reset states and load saved key when provider changes + useEffect(() => { + // Load saved API key from cookies for this provider + const savedKeys = getApiKeysFromCookies(); + const savedKey = savedKeys[provider.name] || ''; + + setTempKey(savedKey); + setApiKey(savedKey); + setIsEditing(false); + }, [provider.name]); + + const checkEnvApiKey = useCallback(async () => { + // Check cache first + if (providerEnvKeyStatusCache[provider.name] !== undefined) { + setIsEnvKeySet(providerEnvKeyStatusCache[provider.name]); + return; + } + + try { + const response = await fetch(`/api/check-env-key?provider=${encodeURIComponent(provider.name)}`); + const data = await response.json(); + const isSet = (data as { isSet: boolean }).isSet; + + // Cache the result + providerEnvKeyStatusCache[provider.name] = isSet; + setIsEnvKeySet(isSet); + } catch (error) { + console.error('Failed to check environment API key:', error); + setIsEnvKeySet(false); + } + }, [provider.name]); + + useEffect(() => { + checkEnvApiKey(); + }, [checkEnvApiKey]); const handleSave = () => { + // Save to parent state setApiKey(tempKey); + + // Save to cookies + const currentKeys = getApiKeysFromCookies(); + const newKeys = { ...currentKeys, [provider.name]: tempKey }; + Cookies.set('apiKeys', JSON.stringify(newKeys)); + setIsEditing(false); }; return ( -
-
- {provider?.name} API Key: - {!isEditing && ( -
- - {apiKey ? '••••••••' : 'Not set (will still work if set in .env file)'} - - setIsEditing(true)} title="Edit API Key"> -
+
+
+
+ {provider?.name} API Key: + {!isEditing && ( +
+ {isEnvKeySet ? ( + <> +
+ + Set via {providerBaseUrlEnvKeys[provider.name].apiTokenKey} environment variable + + + ) : apiKey ? ( + <> +
+ Set via UI + + ) : ( + <> +
+ Not Set (Please set via UI or ENV_VAR) + + )} +
+ )} +
+
+ +
+ {isEditing && !isEnvKeySet ? ( +
+ setTempKey(e.target.value)} + className="w-[300px] px-3 py-1.5 text-sm rounded border border-bolt-elements-borderColor + bg-bolt-elements-prompt-background text-bolt-elements-textPrimary + focus:outline-none focus:ring-2 focus:ring-bolt-elements-focus" + /> + +
+ + setIsEditing(false)} + title="Cancel" + className="bg-red-500/10 hover:bg-red-500/20 text-red-500" + > +
+ ) : ( + <> + {!isEnvKeySet && ( + setIsEditing(true)} + title="Edit API Key" + className="bg-blue-500/10 hover:bg-blue-500/20 text-blue-500" + > +
+ + )} + {provider?.getApiKeyLink && !isEnvKeySet && ( + window.open(provider?.getApiKeyLink)} + title="Get API Key" + className="bg-purple-500/10 hover:bg-purple-500/20 text-purple-500 flex items-center gap-2" + > + {provider?.labelForGetApiKey || 'Get API Key'} +
+ + )} + )}
- - {isEditing ? ( -
- setTempKey(e.target.value)} - className="flex-1 px-2 py-1 text-xs lg:text-sm rounded border border-bolt-elements-borderColor bg-bolt-elements-prompt-background text-bolt-elements-textPrimary focus:outline-none focus:ring-2 focus:ring-bolt-elements-focus" - /> - -
- - setIsEditing(false)} title="Cancel"> -
- -
- ) : ( - <> - {provider?.getApiKeyLink && ( - window.open(provider?.getApiKeyLink)} title="Edit API Key"> - {provider?.labelForGetApiKey || 'Get API Key'} -
- - )} - - )}
); }; diff --git a/app/components/chat/BaseChat.tsx b/app/components/chat/BaseChat.tsx index 7e82b3581..bf995a3d9 100644 --- a/app/components/chat/BaseChat.tsx +++ b/app/components/chat/BaseChat.tsx @@ -184,7 +184,6 @@ export const BaseChat = React.forwardRef( setIsModelLoading('all'); initializeModelList({ apiKeys: parsedApiKeys, providerSettings }) .then((modelList) => { - // console.log('Model List: ', modelList); setModelList(modelList); }) .catch((error) => { @@ -194,7 +193,7 @@ export const BaseChat = React.forwardRef( setIsModelLoading(undefined); }); } - }, [providerList]); + }, [providerList, provider]); const onApiKeysChange = async (providerName: string, apiKey: string) => { const newApiKeys = { ...apiKeys, [providerName]: apiKey }; diff --git a/app/routes/api.check-env-key.ts b/app/routes/api.check-env-key.ts new file mode 100644 index 000000000..7def79f1f --- /dev/null +++ b/app/routes/api.check-env-key.ts @@ -0,0 +1,16 @@ +import type { LoaderFunction } from '@remix-run/node'; +import { providerBaseUrlEnvKeys } from '~/utils/constants'; + +export const loader: LoaderFunction = async ({ context, request }) => { + const url = new URL(request.url); + const provider = url.searchParams.get('provider'); + + if (!provider || !providerBaseUrlEnvKeys[provider].apiTokenKey) { + return Response.json({ isSet: false }); + } + + const envVarName = providerBaseUrlEnvKeys[provider].apiTokenKey; + const isSet = !!(process.env[envVarName] || (context?.cloudflare?.env as Record)?.[envVarName]); + + return Response.json({ isSet }); +};