AliHassan00 commited on
Commit
3c7bf8c
·
1 Parent(s): 8e7220e

feat: add ability to enter API keys in the UI

Browse files
app/components/chat/APIKeyManager.tsx ADDED
@@ -0,0 +1,49 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState } from 'react';
2
+ import { IconButton } from '~/components/ui/IconButton';
3
+
4
+ interface APIKeyManagerProps {
5
+ provider: string;
6
+ apiKey: string;
7
+ setApiKey: (key: string) => void;
8
+ }
9
+
10
+ export const APIKeyManager: React.FC<APIKeyManagerProps> = ({ provider, apiKey, setApiKey }) => {
11
+ const [isEditing, setIsEditing] = useState(false);
12
+ const [tempKey, setTempKey] = useState(apiKey);
13
+
14
+ const handleSave = () => {
15
+ setApiKey(tempKey);
16
+ setIsEditing(false);
17
+ };
18
+
19
+ return (
20
+ <div className="flex items-center gap-2 mt-2 mb-2">
21
+ <span className="text-sm text-bolt-elements-textSecondary">{provider} API Key:</span>
22
+ {isEditing ? (
23
+ <>
24
+ <input
25
+ type="password"
26
+ value={tempKey}
27
+ onChange={(e) => setTempKey(e.target.value)}
28
+ className="flex-1 p-1 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"
29
+ />
30
+ <IconButton onClick={handleSave} title="Save API Key">
31
+ <div className="i-ph:check" />
32
+ </IconButton>
33
+ <IconButton onClick={() => setIsEditing(false)} title="Cancel">
34
+ <div className="i-ph:x" />
35
+ </IconButton>
36
+ </>
37
+ ) : (
38
+ <>
39
+ <span className="flex-1 text-sm text-bolt-elements-textPrimary">
40
+ {apiKey ? '••••••••' : 'Not set'}
41
+ </span>
42
+ <IconButton onClick={() => setIsEditing(true)} title="Edit API Key">
43
+ <div className="i-ph:pencil-simple" />
44
+ </IconButton>
45
+ </>
46
+ )}
47
+ </div>
48
+ );
49
+ };
app/components/chat/BaseChat.tsx CHANGED
@@ -1,7 +1,7 @@
1
  // @ts-nocheck
2
  // Preventing TS checks with files presented in the video for a better presentation.
3
  import type { Message } from 'ai';
4
- import React, { type RefCallback } from 'react';
5
  import { ClientOnly } from 'remix-utils/client-only';
6
  import { Menu } from '~/components/sidebar/Menu.client';
7
  import { IconButton } from '~/components/ui/IconButton';
@@ -11,6 +11,7 @@ import { MODEL_LIST, DEFAULT_PROVIDER } from '~/utils/constants';
11
  import { Messages } from './Messages.client';
12
  import { SendButton } from './SendButton.client';
13
  import { useState } from 'react';
 
14
 
15
  import styles from './BaseChat.module.scss';
16
 
@@ -24,18 +25,17 @@ const EXAMPLE_PROMPTS = [
24
 
25
  const providerList = [...new Set(MODEL_LIST.map((model) => model.provider))]
26
 
27
- const ModelSelector = ({ model, setModel, modelList, providerList }) => {
28
- const [provider, setProvider] = useState(DEFAULT_PROVIDER);
29
  return (
30
- <div className="mb-2">
31
- <select
32
  value={provider}
33
  onChange={(e) => {
34
  setProvider(e.target.value);
35
  const firstModel = [...modelList].find(m => m.provider == e.target.value);
36
  setModel(firstModel ? firstModel.name : '');
37
  }}
38
- className="w-full p-2 rounded-lg border border-bolt-elements-borderColor bg-bolt-elements-prompt-background text-bolt-elements-textPrimary focus:outline-none"
39
  >
40
  {providerList.map((provider) => (
41
  <option key={provider} value={provider}>
@@ -52,7 +52,7 @@ const ModelSelector = ({ model, setModel, modelList, providerList }) => {
52
  <select
53
  value={model}
54
  onChange={(e) => setModel(e.target.value)}
55
- className="w-full p-2 rounded-lg border border-bolt-elements-borderColor bg-bolt-elements-prompt-background text-bolt-elements-textPrimary focus:outline-none"
56
  >
57
  {[...modelList].filter(e => e.provider == provider && e.name).map((modelOption) => (
58
  <option key={modelOption.name} value={modelOption.name}>
@@ -108,6 +108,23 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
108
  ref,
109
  ) => {
110
  const TEXTAREA_MAX_HEIGHT = chatStarted ? 400 : 200;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
111
 
112
  return (
113
  <div
@@ -122,11 +139,11 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
122
  <div ref={scrollRef} className="flex overflow-y-auto w-full h-full">
123
  <div className={classNames(styles.Chat, 'flex flex-col flex-grow min-w-[var(--chat-min-width)] h-full')}>
124
  {!chatStarted && (
125
- <div id="intro" className="mt-[26vh] max-w-chat mx-auto">
126
- <h1 className="text-5xl text-center font-bold text-bolt-elements-textPrimary mb-2">
127
  Where ideas begin
128
  </h1>
129
- <p className="mb-4 text-center text-bolt-elements-textSecondary">
130
  Bring ideas to life in seconds or get help on existing projects.
131
  </p>
132
  </div>
@@ -158,15 +175,22 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
158
  setModel={setModel}
159
  modelList={MODEL_LIST}
160
  providerList={providerList}
 
 
 
 
 
 
 
161
  />
162
  <div
163
  className={classNames(
164
- 'shadow-sm border border-bolt-elements-borderColor bg-bolt-elements-prompt-background backdrop-filter backdrop-blur-[8px] rounded-lg overflow-hidden',
165
  )}
166
  >
167
  <textarea
168
  ref={textareaRef}
169
- className={`w-full pl-4 pt-4 pr-16 focus:outline-none resize-none text-md text-bolt-elements-textPrimary placeholder-bolt-elements-textTertiary bg-transparent`}
170
  onKeyDown={(event) => {
171
  if (event.key === 'Enter') {
172
  if (event.shiftKey) {
@@ -205,12 +229,12 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
205
  />
206
  )}
207
  </ClientOnly>
208
- <div className="flex justify-between text-sm p-4 pt-2">
209
  <div className="flex gap-1 items-center">
210
  <IconButton
211
  title="Enhance prompt"
212
  disabled={input.length === 0 || enhancingPrompt}
213
- className={classNames({
214
  'opacity-100!': enhancingPrompt,
215
  'text-bolt-elements-item-contentAccent! pr-1.5 enabled:hover:bg-bolt-elements-item-backgroundAccent!':
216
  promptEnhanced,
@@ -219,7 +243,7 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
219
  >
220
  {enhancingPrompt ? (
221
  <>
222
- <div className="i-svg-spinners:90-ring-with-bg text-bolt-elements-loader-progress text-xl"></div>
223
  <div className="ml-1.5">Enhancing prompt...</div>
224
  </>
225
  ) : (
@@ -232,7 +256,7 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
232
  </div>
233
  {input.length > 3 ? (
234
  <div className="text-xs text-bolt-elements-textTertiary">
235
- Use <kbd className="kdb">Shift</kbd> + <kbd className="kdb">Return</kbd> for a new line
236
  </div>
237
  ) : null}
238
  </div>
@@ -266,4 +290,4 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
266
  </div>
267
  );
268
  },
269
- );
 
1
  // @ts-nocheck
2
  // Preventing TS checks with files presented in the video for a better presentation.
3
  import type { Message } from 'ai';
4
+ import React, { type RefCallback, useEffect } from 'react';
5
  import { ClientOnly } from 'remix-utils/client-only';
6
  import { Menu } from '~/components/sidebar/Menu.client';
7
  import { IconButton } from '~/components/ui/IconButton';
 
11
  import { Messages } from './Messages.client';
12
  import { SendButton } from './SendButton.client';
13
  import { useState } from 'react';
14
+ import { APIKeyManager } from './APIKeyManager';
15
 
16
  import styles from './BaseChat.module.scss';
17
 
 
25
 
26
  const providerList = [...new Set(MODEL_LIST.map((model) => model.provider))]
27
 
28
+ const ModelSelector = ({ model, setModel, modelList, providerList, provider, setProvider }) => {
 
29
  return (
30
+ <div className="mb-2 flex gap-2">
31
+ <select
32
  value={provider}
33
  onChange={(e) => {
34
  setProvider(e.target.value);
35
  const firstModel = [...modelList].find(m => m.provider == e.target.value);
36
  setModel(firstModel ? firstModel.name : '');
37
  }}
38
+ className="flex-1 p-2 rounded-lg 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 transition-all"
39
  >
40
  {providerList.map((provider) => (
41
  <option key={provider} value={provider}>
 
52
  <select
53
  value={model}
54
  onChange={(e) => setModel(e.target.value)}
55
+ className="flex-1 p-2 rounded-lg 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 transition-all"
56
  >
57
  {[...modelList].filter(e => e.provider == provider && e.name).map((modelOption) => (
58
  <option key={modelOption.name} value={modelOption.name}>
 
108
  ref,
109
  ) => {
110
  const TEXTAREA_MAX_HEIGHT = chatStarted ? 400 : 200;
111
+ const [provider, setProvider] = useState(DEFAULT_PROVIDER);
112
+ const [apiKeys, setApiKeys] = useState<Record<string, string>>({});
113
+
114
+ useEffect(() => {
115
+ // Load API keys from localStorage on component mount
116
+ const storedApiKeys = localStorage.getItem('apiKeys');
117
+ if (storedApiKeys) {
118
+ setApiKeys(JSON.parse(storedApiKeys));
119
+ }
120
+ }, []);
121
+
122
+ const updateApiKey = (provider: string, key: string) => {
123
+ const updatedApiKeys = { ...apiKeys, [provider]: key };
124
+ setApiKeys(updatedApiKeys);
125
+ // Save updated API keys to localStorage
126
+ localStorage.setItem('apiKeys', JSON.stringify(updatedApiKeys));
127
+ };
128
 
129
  return (
130
  <div
 
139
  <div ref={scrollRef} className="flex overflow-y-auto w-full h-full">
140
  <div className={classNames(styles.Chat, 'flex flex-col flex-grow min-w-[var(--chat-min-width)] h-full')}>
141
  {!chatStarted && (
142
+ <div id="intro" className="mt-[26vh] max-w-chat mx-auto text-center">
143
+ <h1 className="text-6xl font-bold text-bolt-elements-textPrimary mb-4 animate-fade-in">
144
  Where ideas begin
145
  </h1>
146
+ <p className="text-xl mb-8 text-bolt-elements-textSecondary animate-fade-in animation-delay-200">
147
  Bring ideas to life in seconds or get help on existing projects.
148
  </p>
149
  </div>
 
175
  setModel={setModel}
176
  modelList={MODEL_LIST}
177
  providerList={providerList}
178
+ provider={provider}
179
+ setProvider={setProvider}
180
+ />
181
+ <APIKeyManager
182
+ provider={provider}
183
+ apiKey={apiKeys[provider] || ''}
184
+ setApiKey={(key) => updateApiKey(provider, key)}
185
  />
186
  <div
187
  className={classNames(
188
+ 'shadow-lg border border-bolt-elements-borderColor bg-bolt-elements-prompt-background backdrop-filter backdrop-blur-[8px] rounded-lg overflow-hidden transition-all',
189
  )}
190
  >
191
  <textarea
192
  ref={textareaRef}
193
+ className={`w-full pl-4 pt-4 pr-16 focus:outline-none focus:ring-2 focus:ring-bolt-elements-focus resize-none text-md text-bolt-elements-textPrimary placeholder-bolt-elements-textTertiary bg-transparent transition-all`}
194
  onKeyDown={(event) => {
195
  if (event.key === 'Enter') {
196
  if (event.shiftKey) {
 
229
  />
230
  )}
231
  </ClientOnly>
232
+ <div className="flex justify-between items-center text-sm p-4 pt-2">
233
  <div className="flex gap-1 items-center">
234
  <IconButton
235
  title="Enhance prompt"
236
  disabled={input.length === 0 || enhancingPrompt}
237
+ className={classNames('transition-all', {
238
  'opacity-100!': enhancingPrompt,
239
  'text-bolt-elements-item-contentAccent! pr-1.5 enabled:hover:bg-bolt-elements-item-backgroundAccent!':
240
  promptEnhanced,
 
243
  >
244
  {enhancingPrompt ? (
245
  <>
246
+ <div className="i-svg-spinners:90-ring-with-bg text-bolt-elements-loader-progress text-xl animate-spin"></div>
247
  <div className="ml-1.5">Enhancing prompt...</div>
248
  </>
249
  ) : (
 
256
  </div>
257
  {input.length > 3 ? (
258
  <div className="text-xs text-bolt-elements-textTertiary">
259
+ Use <kbd className="kdb px-1.5 py-0.5 rounded bg-bolt-elements-background-depth-2">Shift</kbd> + <kbd className="kdb px-1.5 py-0.5 rounded bg-bolt-elements-background-depth-2">Return</kbd> for a new line
260
  </div>
261
  ) : null}
262
  </div>
 
290
  </div>
291
  );
292
  },
293
+ );