codacus commited on
Commit
389eedc
·
unverified ·
1 Parent(s): 55cfd5d

fix: better model loading ui feedback and model list update (#954)

Browse files

* fix: better model loading feedback and model list update

* added load on providersettings update

app/components/chat/BaseChat.tsx CHANGED
@@ -3,7 +3,7 @@
3
  * Preventing TS checks with files presented in the video for a better presentation.
4
  */
5
  import type { Message } from 'ai';
6
- import React, { type RefCallback, useEffect, useState } from 'react';
7
  import { ClientOnly } from 'remix-utils/client-only';
8
  import { Menu } from '~/components/sidebar/Menu.client';
9
  import { IconButton } from '~/components/ui/IconButton';
@@ -31,6 +31,7 @@ import { toast } from 'react-toastify';
31
  import StarterTemplates from './StarterTemplates';
32
  import type { ActionAlert } from '~/types/actions';
33
  import ChatAlert from './ChatAlert';
 
34
 
35
  const TEXTAREA_MIN_HEIGHT = 76;
36
 
@@ -100,26 +101,36 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
100
  ref,
101
  ) => {
102
  const TEXTAREA_MAX_HEIGHT = chatStarted ? 400 : 200;
103
- const [apiKeys, setApiKeys] = useState<Record<string, string>>(() => {
104
- const savedKeys = Cookies.get('apiKeys');
105
-
106
- if (savedKeys) {
107
- try {
108
- return JSON.parse(savedKeys);
109
- } catch (error) {
110
- console.error('Failed to parse API keys from cookies:', error);
111
- return {};
112
- }
113
- }
114
-
115
- return {};
116
- });
117
  const [modelList, setModelList] = useState(MODEL_LIST);
118
  const [isModelSettingsCollapsed, setIsModelSettingsCollapsed] = useState(false);
119
  const [isListening, setIsListening] = useState(false);
120
  const [recognition, setRecognition] = useState<SpeechRecognition | null>(null);
121
  const [transcript, setTranscript] = useState('');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
122
 
 
 
123
  useEffect(() => {
124
  console.log(transcript);
125
  }, [transcript]);
@@ -157,25 +168,7 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
157
  }, []);
158
 
159
  useEffect(() => {
160
- let providerSettings: Record<string, IProviderSetting> | undefined = undefined;
161
-
162
- try {
163
- const savedProviderSettings = Cookies.get('providers');
164
-
165
- if (savedProviderSettings) {
166
- const parsedProviderSettings = JSON.parse(savedProviderSettings);
167
-
168
- if (typeof parsedProviderSettings === 'object' && parsedProviderSettings !== null) {
169
- providerSettings = parsedProviderSettings;
170
- }
171
- }
172
- } catch (error) {
173
- console.error('Error loading Provider Settings from cookies:', error);
174
-
175
- // Clear invalid cookie data
176
- Cookies.remove('providers');
177
- }
178
-
179
  let parsedApiKeys: Record<string, string> | undefined = {};
180
 
181
  try {
@@ -187,12 +180,49 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
187
  // Clear invalid cookie data
188
  Cookies.remove('apiKeys');
189
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
190
 
191
- initializeModelList({ apiKeys: parsedApiKeys, providerSettings }).then((modelList) => {
192
- console.log('Model List: ', modelList);
193
- setModelList(modelList);
194
- });
195
- }, [apiKeys]);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
196
 
197
  const startListening = () => {
198
  if (recognition) {
@@ -381,15 +411,14 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
381
  setProvider={setProvider}
382
  providerList={providerList || (PROVIDER_LIST as ProviderInfo[])}
383
  apiKeys={apiKeys}
 
384
  />
385
  {(providerList || []).length > 0 && provider && (
386
  <APIKeyManager
387
  provider={provider}
388
  apiKey={apiKeys[provider.name] || ''}
389
  setApiKey={(key) => {
390
- const newApiKeys = { ...apiKeys, [provider.name]: key };
391
- setApiKeys(newApiKeys);
392
- Cookies.set('apiKeys', JSON.stringify(newApiKeys));
393
  }}
394
  />
395
  )}
 
3
  * Preventing TS checks with files presented in the video for a better presentation.
4
  */
5
  import type { Message } from 'ai';
6
+ import React, { type RefCallback, useCallback, useEffect, useState } from 'react';
7
  import { ClientOnly } from 'remix-utils/client-only';
8
  import { Menu } from '~/components/sidebar/Menu.client';
9
  import { IconButton } from '~/components/ui/IconButton';
 
31
  import StarterTemplates from './StarterTemplates';
32
  import type { ActionAlert } from '~/types/actions';
33
  import ChatAlert from './ChatAlert';
34
+ import { LLMManager } from '~/lib/modules/llm/manager';
35
 
36
  const TEXTAREA_MIN_HEIGHT = 76;
37
 
 
101
  ref,
102
  ) => {
103
  const TEXTAREA_MAX_HEIGHT = chatStarted ? 400 : 200;
104
+ const [apiKeys, setApiKeys] = useState<Record<string, string>>(getApiKeysFromCookies());
 
 
 
 
 
 
 
 
 
 
 
 
 
105
  const [modelList, setModelList] = useState(MODEL_LIST);
106
  const [isModelSettingsCollapsed, setIsModelSettingsCollapsed] = useState(false);
107
  const [isListening, setIsListening] = useState(false);
108
  const [recognition, setRecognition] = useState<SpeechRecognition | null>(null);
109
  const [transcript, setTranscript] = useState('');
110
+ const [isModelLoading, setIsModelLoading] = useState<string | undefined>('all');
111
+
112
+ const getProviderSettings = useCallback(() => {
113
+ let providerSettings: Record<string, IProviderSetting> | undefined = undefined;
114
+
115
+ try {
116
+ const savedProviderSettings = Cookies.get('providers');
117
+
118
+ if (savedProviderSettings) {
119
+ const parsedProviderSettings = JSON.parse(savedProviderSettings);
120
+
121
+ if (typeof parsedProviderSettings === 'object' && parsedProviderSettings !== null) {
122
+ providerSettings = parsedProviderSettings;
123
+ }
124
+ }
125
+ } catch (error) {
126
+ console.error('Error loading Provider Settings from cookies:', error);
127
+
128
+ // Clear invalid cookie data
129
+ Cookies.remove('providers');
130
+ }
131
 
132
+ return providerSettings;
133
+ }, []);
134
  useEffect(() => {
135
  console.log(transcript);
136
  }, [transcript]);
 
168
  }, []);
169
 
170
  useEffect(() => {
171
+ const providerSettings = getProviderSettings();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
172
  let parsedApiKeys: Record<string, string> | undefined = {};
173
 
174
  try {
 
180
  // Clear invalid cookie data
181
  Cookies.remove('apiKeys');
182
  }
183
+ setIsModelLoading('all');
184
+ initializeModelList({ apiKeys: parsedApiKeys, providerSettings })
185
+ .then((modelList) => {
186
+ console.log('Model List: ', modelList);
187
+ setModelList(modelList);
188
+ })
189
+ .catch((error) => {
190
+ console.error('Error initializing model list:', error);
191
+ })
192
+ .finally(() => {
193
+ setIsModelLoading(undefined);
194
+ });
195
+ }, [providerList]);
196
+
197
+ const onApiKeysChange = async (providerName: string, apiKey: string) => {
198
+ const newApiKeys = { ...apiKeys, [providerName]: apiKey };
199
+ setApiKeys(newApiKeys);
200
+ Cookies.set('apiKeys', JSON.stringify(newApiKeys));
201
+
202
+ const provider = LLMManager.getInstance(import.meta.env || process.env || {}).getProvider(providerName);
203
+
204
+ if (provider && provider.getDynamicModels) {
205
+ setIsModelLoading(providerName);
206
 
207
+ try {
208
+ const providerSettings = getProviderSettings();
209
+ const staticModels = provider.staticModels;
210
+ const dynamicModels = await provider.getDynamicModels(
211
+ newApiKeys,
212
+ providerSettings,
213
+ import.meta.env || process.env || {},
214
+ );
215
+
216
+ setModelList((preModels) => {
217
+ const filteredOutPreModels = preModels.filter((x) => x.provider !== providerName);
218
+ return [...filteredOutPreModels, ...staticModels, ...dynamicModels];
219
+ });
220
+ } catch (error) {
221
+ console.error('Error loading dynamic models:', error);
222
+ }
223
+ setIsModelLoading(undefined);
224
+ }
225
+ };
226
 
227
  const startListening = () => {
228
  if (recognition) {
 
411
  setProvider={setProvider}
412
  providerList={providerList || (PROVIDER_LIST as ProviderInfo[])}
413
  apiKeys={apiKeys}
414
+ modelLoading={isModelLoading}
415
  />
416
  {(providerList || []).length > 0 && provider && (
417
  <APIKeyManager
418
  provider={provider}
419
  apiKey={apiKeys[provider.name] || ''}
420
  setApiKey={(key) => {
421
+ onApiKeysChange(provider.name, key);
 
 
422
  }}
423
  />
424
  )}
app/components/chat/ModelSelector.tsx CHANGED
@@ -10,6 +10,7 @@ interface ModelSelectorProps {
10
  modelList: ModelInfo[];
11
  providerList: ProviderInfo[];
12
  apiKeys: Record<string, string>;
 
13
  }
14
 
15
  export const ModelSelector = ({
@@ -19,6 +20,7 @@ export const ModelSelector = ({
19
  setProvider,
20
  modelList,
21
  providerList,
 
22
  }: ModelSelectorProps) => {
23
  // Load enabled providers from cookies
24
 
@@ -83,14 +85,21 @@ export const ModelSelector = ({
83
  value={model}
84
  onChange={(e) => setModel?.(e.target.value)}
85
  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 lg:max-w-[70%]"
 
86
  >
87
- {[...modelList]
88
- .filter((e) => e.provider == provider?.name && e.name)
89
- .map((modelOption, index) => (
90
- <option key={index} value={modelOption.name}>
91
- {modelOption.label}
92
- </option>
93
- ))}
 
 
 
 
 
 
94
  </select>
95
  </div>
96
  );
 
10
  modelList: ModelInfo[];
11
  providerList: ProviderInfo[];
12
  apiKeys: Record<string, string>;
13
+ modelLoading?: string;
14
  }
15
 
16
  export const ModelSelector = ({
 
20
  setProvider,
21
  modelList,
22
  providerList,
23
+ modelLoading,
24
  }: ModelSelectorProps) => {
25
  // Load enabled providers from cookies
26
 
 
85
  value={model}
86
  onChange={(e) => setModel?.(e.target.value)}
87
  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 lg:max-w-[70%]"
88
+ disabled={modelLoading === 'all' || modelLoading === provider?.name}
89
  >
90
+ {modelLoading == 'all' || modelLoading == provider?.name ? (
91
+ <option key={0} value="">
92
+ Loading...
93
+ </option>
94
+ ) : (
95
+ [...modelList]
96
+ .filter((e) => e.provider == provider?.name && e.name)
97
+ .map((modelOption, index) => (
98
+ <option key={index} value={modelOption.name}>
99
+ {modelOption.label}
100
+ </option>
101
+ ))
102
+ )}
103
  </select>
104
  </div>
105
  );
app/lib/modules/llm/manager.ts CHANGED
@@ -79,9 +79,16 @@ export class LLMManager {
79
  }): Promise<ModelInfo[]> {
80
  const { apiKeys, providerSettings, serverEnv } = options;
81
 
 
 
 
 
 
 
82
  // Get dynamic models from all providers that support them
83
  const dynamicModels = await Promise.all(
84
  Array.from(this._providers.values())
 
85
  .filter(
86
  (provider): provider is BaseProvider & Required<Pick<ProviderInfo, 'getDynamicModels'>> =>
87
  !!provider.getDynamicModels,
 
79
  }): Promise<ModelInfo[]> {
80
  const { apiKeys, providerSettings, serverEnv } = options;
81
 
82
+ let enabledProviders = Array.from(this._providers.values()).map((p) => p.name);
83
+
84
+ if (providerSettings) {
85
+ enabledProviders = enabledProviders.filter((p) => providerSettings[p].enabled);
86
+ }
87
+
88
  // Get dynamic models from all providers that support them
89
  const dynamicModels = await Promise.all(
90
  Array.from(this._providers.values())
91
+ .filter((provider) => enabledProviders.includes(provider.name))
92
  .filter(
93
  (provider): provider is BaseProvider & Required<Pick<ProviderInfo, 'getDynamicModels'>> =>
94
  !!provider.getDynamicModels,
public/icons/Hyperbolic.svg ADDED