mirror of
https://github.com/aljazceru/goose.git
synced 2025-12-22 16:44:21 +01:00
ui: models dropdown (#1860)
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
21
ui/desktop/src/agent/index.ts
Normal file
21
ui/desktop/src/agent/index.ts
Normal 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;
|
||||
}
|
||||
@@ -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,71 +77,77 @@ export default function BottomMenu({
|
||||
{/* Goose Mode Selector Dropdown */}
|
||||
<BottomMenuModeSelection />
|
||||
|
||||
{/* Model Selector Dropdown - Only in development */}
|
||||
<div className="relative flex items-center ml-auto mr-4" ref={dropdownRef}>
|
||||
<div
|
||||
className="flex items-center cursor-pointer"
|
||||
onClick={() => setIsModelMenuOpen(!isModelMenuOpen)}
|
||||
>
|
||||
<span>{(currentModel?.alias ?? currentModel?.name) || 'Select Model'}</span>
|
||||
{isModelMenuOpen ? (
|
||||
<ChevronDown className="w-4 h-4 ml-1" />
|
||||
) : (
|
||||
<ChevronUp className="w-4 h-4 ml-1" />
|
||||
)}
|
||||
</div>
|
||||
{/* 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"
|
||||
onClick={() => setIsModelMenuOpen(!isModelMenuOpen)}
|
||||
>
|
||||
<span>{(currentModel?.alias ?? currentModel?.name) || 'Select Model'}</span>
|
||||
{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="">
|
||||
<ModelRadioList
|
||||
className="divide-y divide-borderSubtle"
|
||||
renderItem={({ model, isSelected, onSelect }) => (
|
||||
<label key={model.alias ?? model.name} className="block cursor-pointer">
|
||||
<div
|
||||
className="flex items-center justify-between p-2 text-textStandard hover:bg-bgSubtle transition-colors"
|
||||
onClick={onSelect}
|
||||
>
|
||||
<div>
|
||||
<p className="text-sm ">{model.alias ?? model.name}</p>
|
||||
<p className="text-xs text-textSubtle">{model.subtext ?? model.provider}</p>
|
||||
</div>
|
||||
<div className="relative">
|
||||
<input
|
||||
type="radio"
|
||||
name="recentModels"
|
||||
value={model.name}
|
||||
checked={isSelected}
|
||||
onChange={onSelect}
|
||||
className="peer sr-only"
|
||||
/>
|
||||
<div
|
||||
className="h-4 w-4 rounded-full border border-gray-400 dark:border-gray-500
|
||||
{/* Dropdown Menu */}
|
||||
{isModelMenuOpen && (
|
||||
<div className="absolute bottom-[24px] right-0 w-[300px] bg-bgApp rounded-lg border border-borderSubtle">
|
||||
<div className="">
|
||||
<ModelRadioList
|
||||
className="divide-y divide-borderSubtle"
|
||||
renderItem={({ model, isSelected, onSelect }) => (
|
||||
<label key={model.alias ?? model.name} className="block cursor-pointer">
|
||||
<div
|
||||
className="flex items-center justify-between p-2 text-textStandard hover:bg-bgSubtle transition-colors"
|
||||
onClick={onSelect}
|
||||
>
|
||||
<div>
|
||||
<p className="text-sm ">{model.alias ?? model.name}</p>
|
||||
<p className="text-xs text-textSubtle">
|
||||
{model.subtext ?? model.provider}
|
||||
</p>
|
||||
</div>
|
||||
<div className="relative">
|
||||
<input
|
||||
type="radio"
|
||||
name="recentModels"
|
||||
value={model.name}
|
||||
checked={isSelected}
|
||||
onChange={onSelect}
|
||||
className="peer sr-only"
|
||||
/>
|
||||
<div
|
||||
className="h-4 w-4 rounded-full border border-gray-400 dark:border-gray-500
|
||||
peer-checked:border-[6px] peer-checked:border-black dark:peer-checked:border-white
|
||||
peer-checked:bg-white dark:peer-checked:bg-black
|
||||
transition-all duration-200 ease-in-out"
|
||||
></div>
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
)}
|
||||
/>
|
||||
<div
|
||||
className="flex items-center justify-between text-textStandard p-2 cursor-pointer hover:bg-bgStandard
|
||||
</label>
|
||||
)}
|
||||
/>
|
||||
<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);
|
||||
setView('settings');
|
||||
}}
|
||||
>
|
||||
<span className="text-sm">Tools and Settings</span>
|
||||
<Sliders className="w-5 h-5 ml-2 rotate-90" />
|
||||
onClick={() => {
|
||||
setIsModelMenuOpen(false);
|
||||
setView('settings');
|
||||
}}
|
||||
>
|
||||
<span className="text-sm">Tools and Settings</span>
|
||||
<Sliders className="w-5 h-5 ml-2 rotate-90" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
160
ui/desktop/src/components/settings_v2/models/index.ts
Normal file
160
ui/desktop/src/components/settings_v2/models/index.ts
Normal 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 };
|
||||
}
|
||||
@@ -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}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user