Adithyan K
commited on
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-
|
43 |
-
<div>
|
44 |
-
<
|
45 |
-
|
46 |
-
|
47 |
-
<
|
48 |
-
{
|
49 |
-
|
50 |
-
|
51 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
+
};
|