Skip to content

Commit 49bb178

Browse files
authored
fix: added ui indicator on how apikeys are set (UI/Env) for api-key-manager component (#732)
* fixed #333 * Added instruction in case api-key is not set. * addressed some of the review changes: 1. moved function definiton to useCallback. 2. added a cache to store the env key status and the api call is made only on a cache miss. * Manages the API-key entered via UI in a better way. - Persist API keys in cookies when entered via UI - Automatically load saved keys when switching between providers - Preserve existing functionality for environment variable based keys * Re-used map from utils/constants file. * Code cleanup - Removed redundant API key init in BaseChat as its already handled by APIKeyManager component.
1 parent 6bf36a9 commit 49bb178

File tree

3 files changed

+143
-41
lines changed

3 files changed

+143
-41
lines changed
Lines changed: 126 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1-
import React, { useState } from 'react';
1+
import React, { useState, useEffect, useCallback } from 'react';
22
import { IconButton } from '~/components/ui/IconButton';
33
import type { ProviderInfo } from '~/types/model';
44
import Cookies from 'js-cookie';
5+
import { providerBaseUrlEnvKeys } from '~/utils/constants';
56

67
interface APIKeyManagerProps {
78
provider: ProviderInfo;
@@ -11,11 +12,14 @@ interface APIKeyManagerProps {
1112
labelForGetApiKey?: string;
1213
}
1314

15+
// cache which stores whether the provider's API key is set via environment variable
16+
const providerEnvKeyStatusCache: Record<string, boolean> = {};
17+
1418
const apiKeyMemoizeCache: { [k: string]: Record<string, string> } = {};
1519

1620
export function getApiKeysFromCookies() {
1721
const storedApiKeys = Cookies.get('apiKeys');
18-
let parsedKeys = {};
22+
let parsedKeys: Record<string, string> = {};
1923

2024
if (storedApiKeys) {
2125
parsedKeys = apiKeyMemoizeCache[storedApiKeys];
@@ -32,54 +36,137 @@ export function getApiKeysFromCookies() {
3236
export const APIKeyManager: React.FC<APIKeyManagerProps> = ({ provider, apiKey, setApiKey }) => {
3337
const [isEditing, setIsEditing] = useState(false);
3438
const [tempKey, setTempKey] = useState(apiKey);
39+
const [isEnvKeySet, setIsEnvKeySet] = useState(false);
40+
41+
// Reset states and load saved key when provider changes
42+
useEffect(() => {
43+
// Load saved API key from cookies for this provider
44+
const savedKeys = getApiKeysFromCookies();
45+
const savedKey = savedKeys[provider.name] || '';
46+
47+
setTempKey(savedKey);
48+
setApiKey(savedKey);
49+
setIsEditing(false);
50+
}, [provider.name]);
51+
52+
const checkEnvApiKey = useCallback(async () => {
53+
// Check cache first
54+
if (providerEnvKeyStatusCache[provider.name] !== undefined) {
55+
setIsEnvKeySet(providerEnvKeyStatusCache[provider.name]);
56+
return;
57+
}
58+
59+
try {
60+
const response = await fetch(`/api/check-env-key?provider=${encodeURIComponent(provider.name)}`);
61+
const data = await response.json();
62+
const isSet = (data as { isSet: boolean }).isSet;
63+
64+
// Cache the result
65+
providerEnvKeyStatusCache[provider.name] = isSet;
66+
setIsEnvKeySet(isSet);
67+
} catch (error) {
68+
console.error('Failed to check environment API key:', error);
69+
setIsEnvKeySet(false);
70+
}
71+
}, [provider.name]);
72+
73+
useEffect(() => {
74+
checkEnvApiKey();
75+
}, [checkEnvApiKey]);
3576

3677
const handleSave = () => {
78+
// Save to parent state
3779
setApiKey(tempKey);
80+
81+
// Save to cookies
82+
const currentKeys = getApiKeysFromCookies();
83+
const newKeys = { ...currentKeys, [provider.name]: tempKey };
84+
Cookies.set('apiKeys', JSON.stringify(newKeys));
85+
3886
setIsEditing(false);
3987
};
4088

4189
return (
42-
<div className="flex items-start sm:items-center mt-2 mb-2 flex-col sm:flex-row">
43-
<div>
44-
<span className="text-sm text-bolt-elements-textSecondary">{provider?.name} API Key:</span>
45-
{!isEditing && (
46-
<div className="flex items-center mb-4">
47-
<span className="flex-1 text-xs text-bolt-elements-textPrimary mr-2">
48-
{apiKey ? '••••••••' : 'Not set (will still work if set in .env file)'}
49-
</span>
50-
<IconButton onClick={() => setIsEditing(true)} title="Edit API Key">
51-
<div className="i-ph:pencil-simple" />
90+
<div className="flex items-center justify-between py-3 px-1">
91+
<div className="flex items-center gap-2 flex-1">
92+
<div className="flex items-center gap-2">
93+
<span className="text-sm font-medium text-bolt-elements-textSecondary">{provider?.name} API Key:</span>
94+
{!isEditing && (
95+
<div className="flex items-center gap-2">
96+
{isEnvKeySet ? (
97+
<>
98+
<div className="i-ph:check-circle-fill text-green-500 w-4 h-4" />
99+
<span className="text-xs text-green-500">
100+
Set via {providerBaseUrlEnvKeys[provider.name].apiTokenKey} environment variable
101+
</span>
102+
</>
103+
) : apiKey ? (
104+
<>
105+
<div className="i-ph:check-circle-fill text-green-500 w-4 h-4" />
106+
<span className="text-xs text-green-500">Set via UI</span>
107+
</>
108+
) : (
109+
<>
110+
<div className="i-ph:x-circle-fill text-red-500 w-4 h-4" />
111+
<span className="text-xs text-red-500">Not Set (Please set via UI or ENV_VAR)</span>
112+
</>
113+
)}
114+
</div>
115+
)}
116+
</div>
117+
</div>
118+
119+
<div className="flex items-center gap-2 shrink-0">
120+
{isEditing && !isEnvKeySet ? (
121+
<div className="flex items-center gap-2">
122+
<input
123+
type="password"
124+
value={tempKey}
125+
placeholder="Enter API Key"
126+
onChange={(e) => setTempKey(e.target.value)}
127+
className="w-[300px] px-3 py-1.5 text-sm rounded border border-bolt-elements-borderColor
128+
bg-bolt-elements-prompt-background text-bolt-elements-textPrimary
129+
focus:outline-none focus:ring-2 focus:ring-bolt-elements-focus"
130+
/>
131+
<IconButton
132+
onClick={handleSave}
133+
title="Save API Key"
134+
className="bg-green-500/10 hover:bg-green-500/20 text-green-500"
135+
>
136+
<div className="i-ph:check w-4 h-4" />
137+
</IconButton>
138+
<IconButton
139+
onClick={() => setIsEditing(false)}
140+
title="Cancel"
141+
className="bg-red-500/10 hover:bg-red-500/20 text-red-500"
142+
>
143+
<div className="i-ph:x w-4 h-4" />
52144
</IconButton>
53145
</div>
146+
) : (
147+
<>
148+
{!isEnvKeySet && (
149+
<IconButton
150+
onClick={() => setIsEditing(true)}
151+
title="Edit API Key"
152+
className="bg-blue-500/10 hover:bg-blue-500/20 text-blue-500"
153+
>
154+
<div className="i-ph:pencil-simple w-4 h-4" />
155+
</IconButton>
156+
)}
157+
{provider?.getApiKeyLink && !isEnvKeySet && (
158+
<IconButton
159+
onClick={() => window.open(provider?.getApiKeyLink)}
160+
title="Get API Key"
161+
className="bg-purple-500/10 hover:bg-purple-500/20 text-purple-500 flex items-center gap-2"
162+
>
163+
<span className="text-xs whitespace-nowrap">{provider?.labelForGetApiKey || 'Get API Key'}</span>
164+
<div className={`${provider?.icon || 'i-ph:key'} w-4 h-4`} />
165+
</IconButton>
166+
)}
167+
</>
54168
)}
55169
</div>
56-
57-
{isEditing ? (
58-
<div className="flex items-center gap-3 mt-2">
59-
<input
60-
type="password"
61-
value={tempKey}
62-
placeholder="Your API Key"
63-
onChange={(e) => setTempKey(e.target.value)}
64-
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"
65-
/>
66-
<IconButton onClick={handleSave} title="Save API Key">
67-
<div className="i-ph:check" />
68-
</IconButton>
69-
<IconButton onClick={() => setIsEditing(false)} title="Cancel">
70-
<div className="i-ph:x" />
71-
</IconButton>
72-
</div>
73-
) : (
74-
<>
75-
{provider?.getApiKeyLink && (
76-
<IconButton className="ml-auto" onClick={() => window.open(provider?.getApiKeyLink)} title="Edit API Key">
77-
<span className="mr-2 text-xs lg:text-sm">{provider?.labelForGetApiKey || 'Get API Key'}</span>
78-
<div className={provider?.icon || 'i-ph:key'} />
79-
</IconButton>
80-
)}
81-
</>
82-
)}
83170
</div>
84171
);
85172
};

app/components/chat/BaseChat.tsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,6 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
184184
setIsModelLoading('all');
185185
initializeModelList({ apiKeys: parsedApiKeys, providerSettings })
186186
.then((modelList) => {
187-
// console.log('Model List: ', modelList);
188187
setModelList(modelList);
189188
})
190189
.catch((error) => {
@@ -194,7 +193,7 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
194193
setIsModelLoading(undefined);
195194
});
196195
}
197-
}, [providerList]);
196+
}, [providerList, provider]);
198197

199198
const onApiKeysChange = async (providerName: string, apiKey: string) => {
200199
const newApiKeys = { ...apiKeys, [providerName]: apiKey };

app/routes/api.check-env-key.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import type { LoaderFunction } from '@remix-run/node';
2+
import { providerBaseUrlEnvKeys } from '~/utils/constants';
3+
4+
export const loader: LoaderFunction = async ({ context, request }) => {
5+
const url = new URL(request.url);
6+
const provider = url.searchParams.get('provider');
7+
8+
if (!provider || !providerBaseUrlEnvKeys[provider].apiTokenKey) {
9+
return Response.json({ isSet: false });
10+
}
11+
12+
const envVarName = providerBaseUrlEnvKeys[provider].apiTokenKey;
13+
const isSet = !!(process.env[envVarName] || (context?.cloudflare?.env as Record<string, any>)?.[envVarName]);
14+
15+
return Response.json({ isSet });
16+
};

0 commit comments

Comments
 (0)