Adithyan K commited on
Commit
49bb178
·
unverified ·
1 Parent(s): 6bf36a9

fix: added ui indicator on how apikeys are set (UI/Env) for api-key-manager component (#732)

Browse files

* 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.

app/components/chat/APIKeyManager.tsx CHANGED
@@ -1,7 +1,8 @@
1
- import React, { useState } from 'react';
2
  import { IconButton } from '~/components/ui/IconButton';
3
  import type { ProviderInfo } from '~/types/model';
4
  import Cookies from 'js-cookie';
 
5
 
6
  interface APIKeyManagerProps {
7
  provider: ProviderInfo;
@@ -11,11 +12,14 @@ interface APIKeyManagerProps {
11
  labelForGetApiKey?: string;
12
  }
13
 
 
 
 
14
  const apiKeyMemoizeCache: { [k: string]: Record<string, string> } = {};
15
 
16
  export function getApiKeysFromCookies() {
17
  const storedApiKeys = Cookies.get('apiKeys');
18
- let parsedKeys = {};
19
 
20
  if (storedApiKeys) {
21
  parsedKeys = apiKeyMemoizeCache[storedApiKeys];
@@ -32,54 +36,137 @@ export function getApiKeysFromCookies() {
32
  export const APIKeyManager: React.FC<APIKeyManagerProps> = ({ provider, apiKey, setApiKey }) => {
33
  const [isEditing, setIsEditing] = useState(false);
34
  const [tempKey, setTempKey] = useState(apiKey);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
35
 
36
  const handleSave = () => {
 
37
  setApiKey(tempKey);
 
 
 
 
 
 
38
  setIsEditing(false);
39
  };
40
 
41
  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" />
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
52
  </IconButton>
53
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
54
  )}
55
  </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
- )}
83
  </div>
84
  );
85
  };
 
1
+ import React, { useState, useEffect, useCallback } from 'react';
2
  import { IconButton } from '~/components/ui/IconButton';
3
  import type { ProviderInfo } from '~/types/model';
4
  import Cookies from 'js-cookie';
5
+ import { providerBaseUrlEnvKeys } from '~/utils/constants';
6
 
7
  interface APIKeyManagerProps {
8
  provider: ProviderInfo;
 
12
  labelForGetApiKey?: string;
13
  }
14
 
15
+ // cache which stores whether the provider's API key is set via environment variable
16
+ const providerEnvKeyStatusCache: Record<string, boolean> = {};
17
+
18
  const apiKeyMemoizeCache: { [k: string]: Record<string, string> } = {};
19
 
20
  export function getApiKeysFromCookies() {
21
  const storedApiKeys = Cookies.get('apiKeys');
22
+ let parsedKeys: Record<string, string> = {};
23
 
24
  if (storedApiKeys) {
25
  parsedKeys = apiKeyMemoizeCache[storedApiKeys];
 
36
  export const APIKeyManager: React.FC<APIKeyManagerProps> = ({ provider, apiKey, setApiKey }) => {
37
  const [isEditing, setIsEditing] = useState(false);
38
  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]);
76
 
77
  const handleSave = () => {
78
+ // Save to parent state
79
  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
+
86
  setIsEditing(false);
87
  };
88
 
89
  return (
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" />
144
  </IconButton>
145
  </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
+ </>
168
  )}
169
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
170
  </div>
171
  );
172
  };
app/components/chat/BaseChat.tsx CHANGED
@@ -184,7 +184,6 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
184
  setIsModelLoading('all');
185
  initializeModelList({ apiKeys: parsedApiKeys, providerSettings })
186
  .then((modelList) => {
187
- // console.log('Model List: ', modelList);
188
  setModelList(modelList);
189
  })
190
  .catch((error) => {
@@ -194,7 +193,7 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
194
  setIsModelLoading(undefined);
195
  });
196
  }
197
- }, [providerList]);
198
 
199
  const onApiKeysChange = async (providerName: string, apiKey: string) => {
200
  const newApiKeys = { ...apiKeys, [providerName]: apiKey };
 
184
  setIsModelLoading('all');
185
  initializeModelList({ apiKeys: parsedApiKeys, providerSettings })
186
  .then((modelList) => {
 
187
  setModelList(modelList);
188
  })
189
  .catch((error) => {
 
193
  setIsModelLoading(undefined);
194
  });
195
  }
196
+ }, [providerList, provider]);
197
 
198
  const onApiKeysChange = async (providerName: string, apiKey: string) => {
199
  const newApiKeys = { ...apiKeys, [providerName]: apiKey };
app/routes/api.check-env-key.ts ADDED
@@ -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
+ };