ui: models dropdown (#1860)

This commit is contained in:
Lily Delalande
2025-03-25 21:06:23 -04:00
committed by GitHub
parent 22e08e5a15
commit f098fed862
13 changed files with 631 additions and 284 deletions

View File

@@ -33,7 +33,7 @@ export const useAgent = () => {
console.error('Failed to initialize agent:', error);
ToastError({
title: 'Failed to initialize agent',
errorMessage: error instanceof Error ? error.message : 'Unknown error',
traceback: error instanceof Error ? error.message : 'Unknown error',
});
return false;
}
@@ -131,7 +131,7 @@ export const useAgent = () => {
ToastError({
title: extension.name,
msg: 'Failed to add extension',
errorMessage: errorMsg,
traceback: errorMsg,
});
}
return response;
@@ -167,7 +167,7 @@ export const useAgent = () => {
ToastError({
title: extension.name,
msg: 'Failed to add extension',
errorMessage: data.message,
traceback: data.message,
});
return response;
@@ -178,7 +178,7 @@ export const useAgent = () => {
ToastError({
title: extension.name,
msg: 'Failed to add extension',
errorMessage: error.message,
traceback: error.message,
});
throw error;
}

View File

@@ -0,0 +1,21 @@
import { getApiUrl, getSecretKey } from '../config';
interface initializeAgentProps {
model: string;
provider: string;
}
export async function initializeAgent({ model, provider }: initializeAgentProps) {
const response = await fetch(getApiUrl('/agent'), {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Secret-Key': getSecretKey(),
},
body: JSON.stringify({
provider: provider.toLowerCase().replace(/ /g, '_'),
model: model,
}),
});
return response;
}

View File

@@ -6,6 +6,7 @@ import { ModelRadioList } from './settings/models/ModelRadioList';
import { Document, ChevronUp, ChevronDown } from './icons';
import type { View } from '../App';
import { BottomMenuModeSelection } from './BottomMenuModeSelection';
import ModelsBottomBar from './settings_v2/models/subcomponents/ModelsBottomBar';
export default function BottomMenu({
hasMessages,
@@ -76,7 +77,10 @@ export default function BottomMenu({
{/* Goose Mode Selector Dropdown */}
<BottomMenuModeSelection />
{/* Model Selector Dropdown - Only in development */}
{/* Model Selector Dropdown */}
{process.env.ALPHA ? (
<ModelsBottomBar dropdownRef={dropdownRef} setView={setView} />
) : (
<div className="relative flex items-center ml-auto mr-4" ref={dropdownRef}>
<div
className="flex items-center cursor-pointer"
@@ -104,7 +108,9 @@ export default function BottomMenu({
>
<div>
<p className="text-sm ">{model.alias ?? model.name}</p>
<p className="text-xs text-textSubtle">{model.subtext ?? model.provider}</p>
<p className="text-xs text-textSubtle">
{model.subtext ?? model.provider}
</p>
</div>
<div className="relative">
<input
@@ -141,6 +147,7 @@ export default function BottomMenu({
</div>
)}
</div>
)}
</div>
);
}

View File

@@ -3,33 +3,8 @@ import { ScrollArea } from '../ui/scroll-area';
import BackButton from '../ui/BackButton';
import type { View } from '../../App';
import { useConfig } from '../ConfigContext';
import { Button } from '../ui/button';
import { Plus, Sliders } from 'lucide-react';
import ExtensionsSection from './extensions/ExtensionsSection';
import { AddModelButton } from './models/AddModelButton';
interface ModelOption {
id: string;
name: string;
description: string;
selected: boolean;
}
// Mock data - replace with actual data source
const defaultModelOptions: ModelOption[] = [
{
id: 'gpt-4',
name: 'GPT-4',
description: 'Most capable model, best for complex tasks',
selected: true,
},
{
id: 'gpt-3.5',
name: 'GPT-3.5',
description: 'Fast and efficient for most tasks',
selected: false,
},
];
import ModelsSection from './models/ModelsSection';
export type SettingsViewOptions = {
extensionId?: string;
@@ -45,21 +20,10 @@ export default function SettingsView({
setView: (view: View) => void;
viewOptions: SettingsViewOptions;
}) {
const [modelOptions, setModelOptions] = React.useState<ModelOption[]>(defaultModelOptions);
const { config } = useConfig();
console.log(config);
const handleModelSelect = (selectedId: string) => {
setModelOptions(
modelOptions.map((model) => ({
...model,
selected: model.id === selectedId,
}))
);
};
return (
<div className="h-screen w-full">
<div className="relative flex items-center h-[36px] w-full bg-bgSubtle"></div>
@@ -74,48 +38,7 @@ export default function SettingsView({
<div className="flex-1 pt-[20px]">
<div className="space-y-8">
{/* Models Section */}
<section id="models">
<div className="flex justify-between items-center mb-6 px-8">
<h1 className="text-3xl font-medium text-textStandard">Models</h1>
</div>
<div className="px-8">
<div className="space-y-2">
{modelOptions.map((model, index) => (
<React.Fragment key={model.id}>
<div className="flex items-center justify-between py-3">
<div className="space-y-1">
<h3 className="font-medium text-textStandard">{model.name}</h3>
<p className="text-sm text-textSubtle">{model.description}</p>
</div>
<input
type="radio"
name="model"
checked={model.selected}
onChange={() => handleModelSelect(model.id)}
className="h-4 w-4 text-white accent-[#393838] bg-[#393838] border-[#393838] checked:bg-[#393838] focus:ring-0 focus:ring-offset-0"
/>
</div>
{index < modelOptions.length - 1 && (
<div className="h-px bg-borderSubtle" />
)}
</React.Fragment>
))}
</div>
<div className="flex gap-4 pt-4 w-full">
<AddModelButton />
<Button
className="flex items-center gap-2 flex-1 justify-center text-textSubtle bg-white dark:bg-black hover:bg-subtle dark:border dark:border-gray-500 dark:hover:border-gray-400"
onClick={() => {
setView('ConfigureProviders');
}}
>
<Sliders className="h-4 w-4 rotate-90" />
Configure Providers
</Button>
</div>
</div>
</section>
<ModelsSection setView={setView} />
{/* Extensions Section */}
<ExtensionsSection />
</div>

View File

@@ -1,21 +0,0 @@
import React, { useState } from 'react';
import { Plus } from 'lucide-react';
import { Button } from '../../ui/button';
import { AddModelModal } from './AddModelModal';
export const AddModelButton = () => {
const [isAddModelModalOpen, setIsAddModelModalOpen] = useState(false);
return (
<>
<Button
className="flex items-center gap-2 flex-1 justify-center text-white dark:text-textSubtle bg-black dark:bg-white hover:bg-subtle"
onClick={() => setIsAddModelModalOpen(true)}
>
<Plus className="h-4 w-4" />
Add Model
</Button>
{isAddModelModalOpen ? <AddModelModal onClose={() => setIsAddModelModalOpen(false)} /> : null}
</>
);
};

View File

@@ -1,122 +0,0 @@
import React, { useEffect, useState } from 'react';
import { ExternalLink, Plus } from 'lucide-react';
import Modal from '../../Modal';
import { Button } from '../../ui/button';
import { QUICKSTART_GUIDE_URL } from '../providers/modal/constants';
import { Input } from '../../ui/input';
import { Select } from '../../ui/Select';
import { useConfig } from '../../ConfigContext';
import { ToastError, ToastSuccess } from '../../settings/models/toasts';
import { initializeSystem } from '../../../../src/utils/providerUtils';
const ModalButtons = ({ onSubmit, onCancel }) => (
<div>
<Button
type="submit"
variant="ghost"
onClick={onSubmit}
className="w-full h-[60px] rounded-none border-borderSubtle text-base hover:bg-bgSubtle text-textProminent font-regular"
>
Add model
</Button>
<Button
type="button"
variant="ghost"
onClick={onCancel}
className="w-full h-[60px] rounded-none border-t border-borderSubtle hover:text-textStandard text-textSubtle hover:bg-bgSubtle text-base font-regular"
>
Cancel
</Button>
</div>
);
type AddModelModalProps = { onClose: () => void };
export const AddModelModal = ({ onClose }: AddModelModalProps) => {
const { getProviders, upsert } = useConfig();
const [providerOptions, setProviderOptions] = useState([]);
const [provider, setProvider] = useState<string | null>(null);
const [modelName, setModelName] = useState<string>('');
const changeModel = async () => {
try {
await upsert('GOOSE_PROVIDER', provider, false);
await upsert('GOOSE_MODEL', modelName, false);
await initializeSystem(provider, modelName);
ToastSuccess({
title: 'Model changed',
msg: `Switched to ${modelName}.`,
});
onClose();
} catch (e) {
ToastError({
title: 'Failed to add model',
traceback: e.message,
});
}
};
useEffect(() => {
(async () => {
try {
const providersResponse = await getProviders(false);
const activeProviders = providersResponse.filter((provider) => provider.is_configured);
setProviderOptions(
activeProviders.map(({ metadata, name }) => ({
value: name,
label: metadata.display_name,
}))
);
} catch (error) {
console.error('Failed to load providers:', error);
}
})();
}, [getProviders]);
return (
<div className="z-10">
<Modal onClose={onClose} footer={<ModalButtons onSubmit={changeModel} onCancel={onClose} />}>
<div className="flex flex-col items-center gap-8">
<div className="flex flex-col items-center gap-3">
<Plus size={24} className="text-textStandard" />
<div className="text-textStandard font-medium">Add model</div>
<div className="text-textSubtle text-center">
Configure your AI model providers by adding their API keys. your Keys are stored
securely and encrypted locally.
</div>
<div>
<a
href={QUICKSTART_GUIDE_URL}
target="_blank"
rel="noopener noreferrer"
className="flex items-center justify-center text-textStandard font-medium text-sm"
>
<ExternalLink size={16} className="mr-1" />
View quick start guide
</a>
</div>
</div>
<div className="w-full flex flex-col gap-4">
<Select
options={providerOptions}
value={providerOptions.find((option) => option.value === provider) || null}
onChange={(option) => {
setProvider(option?.value || null);
setModelName('');
}}
placeholder="Provider"
isClearable
/>
<Input
className="border-2 px-4 py-5"
placeholder="GPT"
onChange={(event) => setModelName(event.target.value)}
value={modelName}
/>
</div>
</div>
</Modal>
</div>
);
};

View File

@@ -0,0 +1,61 @@
import React, { useEffect, useState } from 'react';
import type { View } from '../../../App';
import ModelSettingsButtons from './subcomponents/ModelSettingsButtons';
import { useConfig } from '../../ConfigContext';
import { ToastError } from '../../settings/models/toasts';
interface ModelsSectionProps {
setView: (view: View) => void;
}
const UNKNOWN_PROVIDER_TITLE = 'Provider name error';
const UNKNOWN_PROVIDER_MSG = 'Unknown provider in config -- please inspect your config.yaml';
// todo: use for block settings
export default function ModelsSection({ setView }: ModelsSectionProps) {
const [provider, setProvider] = useState<string | null>(null);
const [model, setModel] = useState<string>('');
const { read, getProviders } = useConfig();
useEffect(() => {
const currentModel = async () => {
const gooseModel = (await read('GOOSE_MODEL', false)) as string;
const gooseProvider = (await read('GOOSE_PROVIDER', false)) as string;
const providers = await getProviders(true);
// lookup display name
const providerDetailsList = providers.filter((provider) => provider.name === gooseProvider);
if (providerDetailsList.length != 1) {
ToastError({
title: UNKNOWN_PROVIDER_TITLE,
msg: UNKNOWN_PROVIDER_MSG,
});
setModel(gooseModel);
setProvider(gooseProvider);
return;
}
const providerDisplayName = providerDetailsList[0].metadata.display_name;
setModel(gooseModel);
setProvider(providerDisplayName);
};
(async () => {
await currentModel();
})();
}, [getProviders, read]);
return (
<section id="models">
<div className="flex justify-between items-center mb-6 px-8">
<h1 className="text-3xl font-medium text-textStandard">Models</h1>
</div>
<div className="px-8">
<div className="space-y-2">
<h3 className="font-medium text-textStandard">{model}</h3>
<h4 className="font-medium text-textSubtle">{provider}</h4>
</div>
<ModelSettingsButtons setView={setView} />
</div>
</section>
);
}

View File

@@ -0,0 +1,160 @@
import { initializeAgent } from '../../../agent/index';
import { ToastError, ToastSuccess } from '../../settings/models/toasts';
import { ProviderDetails } from '@/src/api';
// titles
const CHANGE_MODEL_TOAST_TITLE = 'Model selected';
const START_AGENT_TITLE = 'Initialize agent';
const UNKNOWN_PROVIDER_TITLE = 'Provider name lookup';
// errors
const SWITCH_MODEL_AGENT_ERROR_MSG = 'Failed to start agent with selected model';
const CONFIG_UPDATE_ERROR_MSG = 'Failed to update configuration settings';
const CONFIG_READ_MODEL_ERROR_MSG = 'Failed to read GOOSE_MODEL or GOOSE_PROVIDER from config';
const UNKNOWN_PROVIDER_MSG = 'Unknown provider in config -- please inspect your config.yaml';
// success
const SWITCH_MODEL_SUCCESS_MSG = 'Successfully switched models';
const INITIALIZE_SYSTEM_WITH_MODEL_SUCCESS_MSG = 'Successfully started Goose';
interface changeModelProps {
model: string;
provider: string;
writeToConfig: (key: string, value: unknown, is_secret: boolean) => Promise<void>;
}
// TODO: error handling
export async function changeModel({ model, provider, writeToConfig }: changeModelProps) {
try {
await initializeAgent({ model: model, provider: provider });
} catch (error) {
console.error(`Failed to change model at agent step -- ${model} ${provider}`);
// show toast with error
ToastError({
title: CHANGE_MODEL_TOAST_TITLE,
msg: SWITCH_MODEL_AGENT_ERROR_MSG,
traceback: error,
});
// don't write to config
return;
}
try {
await writeToConfig('GOOSE_PROVIDER', provider, false);
await writeToConfig('GOOSE_MODEL', model, false);
} catch (error) {
console.error(`Failed to change model at config step -- ${model} ${provider}`);
// show toast with error
ToastError({
title: CHANGE_MODEL_TOAST_TITLE,
msg: CONFIG_UPDATE_ERROR_MSG,
traceback: error,
});
// agent and config will be out of sync at this point
// TODO: reset agent to use current config settings
} finally {
// show toast
ToastSuccess({
title: CHANGE_MODEL_TOAST_TITLE,
msg: `${SWITCH_MODEL_SUCCESS_MSG} -- using ${model} from ${provider}`,
});
}
}
interface startAgentFromConfigProps {
readFromConfig: (key: string, is_secret: boolean) => Promise<unknown>;
}
// starts agent with the values for GOOSE_PROVIDER and GOOSE_MODEL that are in the config
export async function startAgentFromConfig({ readFromConfig }: startAgentFromConfigProps) {
let modelProvider: { model: string; provider: string };
// read from config
try {
modelProvider = await getCurrentModelAndProvider({ readFromConfig: readFromConfig });
} catch (error) {
// show toast with error
ToastError({
title: START_AGENT_TITLE,
msg: CONFIG_READ_MODEL_ERROR_MSG,
traceback: error,
});
return;
}
const model = modelProvider.model;
const provider = modelProvider.provider;
console.log(`Starting agent with GOOSE_MODEL=${model} and GOOSE_PROVIDER=${provider}`);
try {
await initializeAgent({ model: model, provider: provider });
} catch (error) {
console.error(`Failed to change model at agent step -- ${model} ${provider}`);
// show toast with error
ToastError({
title: CHANGE_MODEL_TOAST_TITLE,
msg: SWITCH_MODEL_AGENT_ERROR_MSG,
traceback: error,
});
return;
} finally {
// success toast
ToastSuccess({
title: CHANGE_MODEL_TOAST_TITLE,
msg: `${INITIALIZE_SYSTEM_WITH_MODEL_SUCCESS_MSG} with ${model} from ${provider}`,
});
}
}
interface getCurrentModelAndProviderProps {
readFromConfig: (key: string, is_secret: boolean) => Promise<unknown>;
}
export async function getCurrentModelAndProvider({
readFromConfig,
}: getCurrentModelAndProviderProps) {
let model: string;
let provider: string;
// read from config
try {
model = (await readFromConfig('GOOSE_MODEL', false)) as string;
provider = (await readFromConfig('GOOSE_PROVIDER', false)) as string;
} catch (error) {
console.error(`Failed to read GOOSE_MODEL or GOOSE_PROVIDER from config`);
throw error;
}
return { model: model, provider: provider };
}
interface getCurrentModelAndProviderForDisplayProps {
readFromConfig: (key: string, is_secret: boolean) => Promise<unknown>;
getProviders: (b: boolean) => Promise<ProviderDetails[]>;
}
// returns display name of the provider
export async function getCurrentModelAndProviderForDisplay({
readFromConfig,
getProviders,
}: getCurrentModelAndProviderForDisplayProps) {
const modelProvider = await getCurrentModelAndProvider({ readFromConfig: readFromConfig });
const gooseModel = modelProvider.model;
const gooseProvider = modelProvider.provider;
const providers = await getProviders(false);
// lookup display name
const providerDetailsList = providers.filter((provider) => provider.name === gooseProvider);
if (providerDetailsList.length != 1) {
ToastError({
title: UNKNOWN_PROVIDER_TITLE,
msg: UNKNOWN_PROVIDER_MSG,
});
return { model: gooseModel, provider: gooseProvider };
}
const providerDisplayName = providerDetailsList[0].metadata.display_name;
return { model: gooseModel, provider: providerDisplayName };
}

View File

@@ -0,0 +1,28 @@
import React, { useState } from 'react';
import { Button } from '../../../ui/button';
import { AddModelModal } from './AddModelModal';
import { Gear } from '../../../icons';
import type { View } from '../../../../App';
interface AddModelButtonProps {
setView: (view: View) => void;
}
export const AddModelButton = ({ setView }: AddModelButtonProps) => {
const [isAddModelModalOpen, setIsAddModelModalOpen] = useState(false);
return (
<>
<Button
className="flex items-center gap-2 flex-1 justify-center text-white dark:text-textSubtle bg-black dark:bg-white hover:bg-subtle"
onClick={() => setIsAddModelModalOpen(true)}
>
<Gear className="h-4 w-4" />
Switch Models
</Button>
{isAddModelModalOpen ? (
<AddModelModal setView={setView} onClose={() => setIsAddModelModalOpen(false)} />
) : null}
</>
);
};

View File

@@ -0,0 +1,192 @@
import React, { useEffect, useState } from 'react';
import { ExternalLink, Plus } from 'lucide-react';
import Modal from '../../../Modal';
import { Button } from '../../../ui/button';
import { QUICKSTART_GUIDE_URL } from '../../providers/modal/constants';
import { Input } from '../../../ui/input';
import { Select } from '../../../ui/Select';
import { useConfig } from '../../../ConfigContext';
import { changeModel as switchModel } from '../index';
import type { View } from '../../../../App';
const ModalButtons = ({ onSubmit, onCancel }) => (
<div>
<Button
type="submit"
variant="ghost"
onClick={onSubmit}
className="w-full h-[60px] rounded-none border-borderSubtle text-base hover:bg-bgSubtle text-textProminent font-regular"
>
Add model
</Button>
<Button
type="button"
variant="ghost"
onClick={onCancel}
className="w-full h-[60px] rounded-none border-t border-borderSubtle hover:text-textStandard text-textSubtle hover:bg-bgSubtle text-base font-regular"
>
Cancel
</Button>
</div>
);
type AddModelModalProps = {
onClose: () => void;
setView: (view: View) => void;
};
export const AddModelModal = ({ onClose, setView }: AddModelModalProps) => {
const { getProviders, upsert } = useConfig();
const [providerOptions, setProviderOptions] = useState([]);
const [modelOptions, setModelOptions] = useState([]);
const [provider, setProvider] = useState<string | null>(null);
const [model, setModel] = useState<string>('');
const [isCustomModel, setIsCustomModel] = useState(false);
const changeModel = async () => {
await switchModel({ model: model, provider: provider, writeToConfig: upsert });
};
useEffect(() => {
(async () => {
try {
const providersResponse = await getProviders(false);
const activeProviders = providersResponse.filter((provider) => provider.is_configured);
// Create provider options and add "Use other provider" option
setProviderOptions([
...activeProviders.map(({ metadata, name }) => ({
value: name,
label: metadata.display_name,
})),
{
value: 'configure_providers',
label: 'Use other provider',
},
]);
// Format model options by provider
const formattedModelOptions = [];
activeProviders.forEach(({ metadata, name }) => {
if (metadata.known_models && metadata.known_models.length > 0) {
formattedModelOptions.push({
label: metadata.display_name,
options: metadata.known_models.map((modelName) => ({
value: modelName,
label: modelName,
provider: name,
})),
});
}
});
// Add the "Custom model" option to each provider group
formattedModelOptions.forEach((group) => {
group.options.push({
value: 'custom',
label: 'Use custom model',
provider: group.options[0]?.provider,
});
});
setModelOptions(formattedModelOptions);
} catch (error) {
console.error('Failed to load providers:', error);
}
})();
}, [getProviders]);
// Filter model options based on selected provider
const filteredModelOptions = provider
? modelOptions.filter((group) => group.options[0]?.provider === provider)
: [];
// Handle model selection change
const handleModelChange = (selectedOption) => {
if (selectedOption?.value === 'custom') {
setIsCustomModel(true);
setModel('');
} else {
setIsCustomModel(false);
setModel(selectedOption?.value || '');
}
};
return (
<div className="z-10">
<Modal onClose={onClose} footer={<ModalButtons onSubmit={changeModel} onCancel={onClose} />}>
<div className="flex flex-col items-center gap-8">
<div className="flex flex-col items-center gap-3">
<Plus size={24} className="text-textStandard" />
<div className="text-textStandard font-medium">Add model</div>
<div className="text-textSubtle text-center">
Configure your AI model providers by adding their API keys. Your keys are stored
securely and encrypted locally.
</div>
<div>
<a
href={QUICKSTART_GUIDE_URL}
target="_blank"
rel="noopener noreferrer"
className="flex items-center justify-center text-textStandard font-medium text-sm"
>
<ExternalLink size={16} className="mr-1" />
View quick start guide
</a>
</div>
</div>
<div className="w-full flex flex-col gap-4">
<Select
options={providerOptions}
value={providerOptions.find((option) => option.value === provider) || null}
onChange={(option) => {
if (option?.value === 'configure_providers') {
// Navigate to ConfigureProviders view
setView('ConfigureProviders');
onClose(); // Close the current modal
} else {
setProvider(option?.value || null);
setModel('');
setIsCustomModel(false);
}
}}
placeholder="Provider"
isClearable
/>
{provider && (
<>
{!isCustomModel ? (
<Select
options={filteredModelOptions}
onChange={handleModelChange}
value={model ? { value: model, label: model } : null}
placeholder="Select a model"
/>
) : (
<div className="flex flex-col gap-2">
<div className="flex justify-between">
<label className="text-sm text-textSubtle">Custom model name</label>
<button
onClick={() => setIsCustomModel(false)}
className="text-sm text-blue-500 hover:text-blue-700"
>
Back to model list
</button>
</div>
<Input
className="border-2 px-4 py-5"
placeholder="Type model name here"
onChange={(event) => setModel(event.target.value)}
value={model}
/>
</div>
)}
</>
)}
</div>
</div>
</Modal>
</div>
);
};

View File

@@ -0,0 +1,26 @@
import { AddModelButton } from './AddModelButton';
import { Button } from '../../../ui/button';
import { Sliders } from 'lucide-react';
import React from 'react';
import type { View } from '../../../../App';
interface ConfigureModelButtonsProps {
setView: (view: View) => void;
}
export default function ModelSettingsButtons({ setView }: ConfigureModelButtonsProps) {
return (
<div className="flex gap-4 pt-4 w-full">
<AddModelButton setView={setView} />
<Button
className="flex items-center gap-2 flex-1 justify-center text-textSubtle bg-white dark:bg-black hover:bg-subtle dark:border dark:border-gray-500 dark:hover:border-gray-400"
onClick={() => {
setView('ConfigureProviders');
}}
>
<Sliders className="h-4 w-4 rotate-90" />
Configure Providers
</Button>
</div>
);
}

View File

@@ -0,0 +1,72 @@
import { ChevronDown, ChevronUp } from '../../../icons';
import { Sliders } from 'lucide-react';
import React, { useEffect, useState } from 'react';
import { useConfig } from '../../../ConfigContext';
import { getCurrentModelAndProviderForDisplay } from '../index';
import { AddModelModal } from './AddModelModal';
import type { View } from '../../../../App';
interface ModelsBottomBarProps {
dropdownRef: any;
setView: (view: View) => void;
}
export default function ModelsBottomBar({ dropdownRef, setView }: ModelsBottomBarProps) {
const { read, getProviders } = useConfig();
const [isModelMenuOpen, setIsModelMenuOpen] = useState(false);
const [provider, setProvider] = useState<string | null>(null);
const [model, setModel] = useState<string>('');
const [isAddModelModalOpen, setIsAddModelModalOpen] = useState(false);
useEffect(() => {
(async () => {
const modelProvider = await getCurrentModelAndProviderForDisplay({
readFromConfig: read,
getProviders,
});
setProvider(modelProvider.provider);
setModel(modelProvider.model);
})();
}, [read, getProviders]);
return (
<div className="relative flex items-center ml-auto mr-4" ref={dropdownRef}>
<div
className="flex items-center cursor-pointer"
onClick={() => setIsModelMenuOpen(!isModelMenuOpen)}
>
{model}
{isModelMenuOpen ? (
<ChevronDown className="w-4 h-4 ml-1" />
) : (
<ChevronUp className="w-4 h-4 ml-1" />
)}
</div>
{/* Dropdown Menu */}
{isModelMenuOpen && (
<div className="absolute bottom-[24px] right-0 w-[300px] bg-bgApp rounded-lg border border-borderSubtle">
<div className="">
<div className="text-sm text-textProminent mt-3 ml-2">Current:</div>
<div className="flex items-center justify-between text-sm ml-2">
{model} -- {provider}
</div>
<div
className="flex items-center justify-between text-textStandard p-2 cursor-pointer hover:bg-bgStandard
border-t border-borderSubtle mt-2"
onClick={() => {
setIsModelMenuOpen(false);
setIsAddModelModalOpen(true);
}}
>
<span className="text-sm">Change Model</span>
<Sliders className="w-5 h-5 ml-2 rotate-90" />
</div>
</div>
</div>
)}
{isAddModelModalOpen ? (
<AddModelModal setView={setView} onClose={() => setIsAddModelModalOpen(false)} />
) : null}
</div>
);
}

View File

@@ -260,7 +260,7 @@ function handleError(message: string, shouldThrow = false): void {
ToastError({
title: 'Failed to install extension',
msg: message,
errorMessage: message,
traceback: message,
toastOptions: { autoClose: false },
});
console.error(message);