Skip to content

Commit 389eedc

Browse files
authored
fix: better model loading ui feedback and model list update (#954)
* fix: better model loading feedback and model list update * added load on providersettings update
1 parent 55cfd5d commit 389eedc

File tree

4 files changed

+97
-49
lines changed

4 files changed

+97
-49
lines changed

app/components/chat/BaseChat.tsx

Lines changed: 71 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
* Preventing TS checks with files presented in the video for a better presentation.
44
*/
55
import type { Message } from 'ai';
6-
import React, { type RefCallback, useEffect, useState } from 'react';
6+
import React, { type RefCallback, useCallback, useEffect, useState } from 'react';
77
import { ClientOnly } from 'remix-utils/client-only';
88
import { Menu } from '~/components/sidebar/Menu.client';
99
import { IconButton } from '~/components/ui/IconButton';
@@ -31,6 +31,7 @@ import { toast } from 'react-toastify';
3131
import StarterTemplates from './StarterTemplates';
3232
import type { ActionAlert } from '~/types/actions';
3333
import ChatAlert from './ChatAlert';
34+
import { LLMManager } from '~/lib/modules/llm/manager';
3435

3536
const TEXTAREA_MIN_HEIGHT = 76;
3637

@@ -100,26 +101,36 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
100101
ref,
101102
) => {
102103
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-
});
104+
const [apiKeys, setApiKeys] = useState<Record<string, string>>(getApiKeysFromCookies());
117105
const [modelList, setModelList] = useState(MODEL_LIST);
118106
const [isModelSettingsCollapsed, setIsModelSettingsCollapsed] = useState(false);
119107
const [isListening, setIsListening] = useState(false);
120108
const [recognition, setRecognition] = useState<SpeechRecognition | null>(null);
121109
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+
}
122131

132+
return providerSettings;
133+
}, []);
123134
useEffect(() => {
124135
console.log(transcript);
125136
}, [transcript]);
@@ -157,25 +168,7 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
157168
}, []);
158169

159170
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-
171+
const providerSettings = getProviderSettings();
179172
let parsedApiKeys: Record<string, string> | undefined = {};
180173

181174
try {
@@ -187,12 +180,49 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
187180
// Clear invalid cookie data
188181
Cookies.remove('apiKeys');
189182
}
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);
190206

191-
initializeModelList({ apiKeys: parsedApiKeys, providerSettings }).then((modelList) => {
192-
console.log('Model List: ', modelList);
193-
setModelList(modelList);
194-
});
195-
}, [apiKeys]);
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+
};
196226

197227
const startListening = () => {
198228
if (recognition) {
@@ -381,15 +411,14 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
381411
setProvider={setProvider}
382412
providerList={providerList || (PROVIDER_LIST as ProviderInfo[])}
383413
apiKeys={apiKeys}
414+
modelLoading={isModelLoading}
384415
/>
385416
{(providerList || []).length > 0 && provider && (
386417
<APIKeyManager
387418
provider={provider}
388419
apiKey={apiKeys[provider.name] || ''}
389420
setApiKey={(key) => {
390-
const newApiKeys = { ...apiKeys, [provider.name]: key };
391-
setApiKeys(newApiKeys);
392-
Cookies.set('apiKeys', JSON.stringify(newApiKeys));
421+
onApiKeysChange(provider.name, key);
393422
}}
394423
/>
395424
)}

app/components/chat/ModelSelector.tsx

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ interface ModelSelectorProps {
1010
modelList: ModelInfo[];
1111
providerList: ProviderInfo[];
1212
apiKeys: Record<string, string>;
13+
modelLoading?: string;
1314
}
1415

1516
export const ModelSelector = ({
@@ -19,6 +20,7 @@ export const ModelSelector = ({
1920
setProvider,
2021
modelList,
2122
providerList,
23+
modelLoading,
2224
}: ModelSelectorProps) => {
2325
// Load enabled providers from cookies
2426

@@ -83,14 +85,21 @@ export const ModelSelector = ({
8385
value={model}
8486
onChange={(e) => setModel?.(e.target.value)}
8587
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}
8689
>
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-
))}
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+
)}
94103
</select>
95104
</div>
96105
);

app/lib/modules/llm/manager.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,9 +79,16 @@ export class LLMManager {
7979
}): Promise<ModelInfo[]> {
8080
const { apiKeys, providerSettings, serverEnv } = options;
8181

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+
8288
// Get dynamic models from all providers that support them
8389
const dynamicModels = await Promise.all(
8490
Array.from(this._providers.values())
91+
.filter((provider) => enabledProviders.includes(provider.name))
8592
.filter(
8693
(provider): provider is BaseProvider & Required<Pick<ProviderInfo, 'getDynamicModels'>> =>
8794
!!provider.getDynamicModels,

public/icons/Hyperbolic.svg

Lines changed: 3 additions & 0 deletions
Loading

0 commit comments

Comments
 (0)