From e26fa51b3be6b66ee7df143b0d4c8ad6ee88abec Mon Sep 17 00:00:00 2001 From: Lily Delalande Date: Tue, 25 Mar 2025 15:43:16 -0400 Subject: [PATCH 1/8] pulled main --- ui/desktop/src/agent/UpdateAgent.tsx | 8 +- ui/desktop/src/agent/index.ts | 21 ++++ .../components/settings_v2/models/index.ts | 106 ++++++++++++++++++ ui/desktop/src/extensions.tsx | 2 +- 4 files changed, 132 insertions(+), 5 deletions(-) create mode 100644 ui/desktop/src/agent/index.ts create mode 100644 ui/desktop/src/components/settings_v2/models/index.ts diff --git a/ui/desktop/src/agent/UpdateAgent.tsx b/ui/desktop/src/agent/UpdateAgent.tsx index 09ad1ce1d8eb..d44d741316f8 100644 --- a/ui/desktop/src/agent/UpdateAgent.tsx +++ b/ui/desktop/src/agent/UpdateAgent.tsx @@ -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; } diff --git a/ui/desktop/src/agent/index.ts b/ui/desktop/src/agent/index.ts new file mode 100644 index 000000000000..a63bd26e89db --- /dev/null +++ b/ui/desktop/src/agent/index.ts @@ -0,0 +1,21 @@ +import { getApiUrl, getSecretKey } from '@/src/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; +} diff --git a/ui/desktop/src/components/settings_v2/models/index.ts b/ui/desktop/src/components/settings_v2/models/index.ts new file mode 100644 index 000000000000..519972be6659 --- /dev/null +++ b/ui/desktop/src/components/settings_v2/models/index.ts @@ -0,0 +1,106 @@ +import { getApiUrl, getSecretKey } from '@/src/config'; +import { initializeAgent } from '../../../agent/index'; +import { ToastError, ToastLoading, ToastSuccess } from '../../settings/models/toasts'; + +// titles +const CHANGE_MODEL_TOAST_TITLE = 'Model selected'; +const START_AGENT_TITLE = 'Initialize agent'; + +// 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'; + +// 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; +} + +// 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; +} + +// starts agent with the values for GOOSE_PROVIDER and GOOSE_MODEL that are in the config +export async function startAgentFromConfig({ readFromConfig }: startAgentFromConfigProps) { + 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`); + // show toast with error + ToastError({ + title: START_AGENT_TITLE, + msg: CONFIG_READ_MODEL_ERROR_MSG, + traceback: error, + }); + return; + } + + 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}`, + }); + } +} diff --git a/ui/desktop/src/extensions.tsx b/ui/desktop/src/extensions.tsx index 0c607ca4d777..611b1abe36bc 100644 --- a/ui/desktop/src/extensions.tsx +++ b/ui/desktop/src/extensions.tsx @@ -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); From 1d9dd869f5c1c9dea4e86b41b4b424958ce0a942 Mon Sep 17 00:00:00 2001 From: Lily Delalande Date: Tue, 25 Mar 2025 17:27:11 -0400 Subject: [PATCH 2/8] move into sub-components --- ui/desktop/src/agent/index.ts | 2 +- .../components/settings_v2/SettingsView.tsx | 81 +------------------ .../settings_v2/models/ModelsSection.tsx | 61 ++++++++++++++ .../components/settings_v2/models/index.ts | 3 +- .../{ => subcomponents}/AddModelButton.tsx | 8 +- .../{ => subcomponents}/AddModelModal.tsx | 47 +++++------ .../subcomponents/ConfigureModelButtons.tsx | 26 ++++++ 7 files changed, 115 insertions(+), 113 deletions(-) create mode 100644 ui/desktop/src/components/settings_v2/models/ModelsSection.tsx rename ui/desktop/src/components/settings_v2/models/{ => subcomponents}/AddModelButton.tsx (79%) rename ui/desktop/src/components/settings_v2/models/{ => subcomponents}/AddModelModal.tsx (75%) create mode 100644 ui/desktop/src/components/settings_v2/models/subcomponents/ConfigureModelButtons.tsx diff --git a/ui/desktop/src/agent/index.ts b/ui/desktop/src/agent/index.ts index a63bd26e89db..0e80b9577391 100644 --- a/ui/desktop/src/agent/index.ts +++ b/ui/desktop/src/agent/index.ts @@ -1,4 +1,4 @@ -import { getApiUrl, getSecretKey } from '@/src/config'; +import { getApiUrl, getSecretKey } from '../config'; interface initializeAgentProps { model: string; diff --git a/ui/desktop/src/components/settings_v2/SettingsView.tsx b/ui/desktop/src/components/settings_v2/SettingsView.tsx index 07a017c1d988..c0362712d4ab 100644 --- a/ui/desktop/src/components/settings_v2/SettingsView.tsx +++ b/ui/desktop/src/components/settings_v2/SettingsView.tsx @@ -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(defaultModelOptions); - const { config } = useConfig(); console.log(config); - const handleModelSelect = (selectedId: string) => { - setModelOptions( - modelOptions.map((model) => ({ - ...model, - selected: model.id === selectedId, - })) - ); - }; - return (
@@ -74,48 +38,7 @@ export default function SettingsView({
{/* Models Section */} -
-
-

Models

-
-
-
- {modelOptions.map((model, index) => ( - -
-
-

{model.name}

-

{model.description}

-
- 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" - /> -
- {index < modelOptions.length - 1 && ( -
- )} - - ))} -
-
- - -
-
-
- + {/* Extensions Section */}
diff --git a/ui/desktop/src/components/settings_v2/models/ModelsSection.tsx b/ui/desktop/src/components/settings_v2/models/ModelsSection.tsx new file mode 100644 index 000000000000..47a5ea68f452 --- /dev/null +++ b/ui/desktop/src/components/settings_v2/models/ModelsSection.tsx @@ -0,0 +1,61 @@ +import React, { useEffect, useState } from 'react'; +import type { View } from '../../../App'; +import ConfigureModelButtons from './subcomponents/ConfigureModelButtons'; +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(null); + const [model, setModel] = useState(''); + 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 ( +
+
+

Models

+
+
+
+

{model}

+

{provider}

+
+ +
+
+ ); +} diff --git a/ui/desktop/src/components/settings_v2/models/index.ts b/ui/desktop/src/components/settings_v2/models/index.ts index 519972be6659..018adadb8937 100644 --- a/ui/desktop/src/components/settings_v2/models/index.ts +++ b/ui/desktop/src/components/settings_v2/models/index.ts @@ -1,6 +1,5 @@ -import { getApiUrl, getSecretKey } from '@/src/config'; import { initializeAgent } from '../../../agent/index'; -import { ToastError, ToastLoading, ToastSuccess } from '../../settings/models/toasts'; +import { ToastError, ToastSuccess } from '../../settings/models/toasts'; // titles const CHANGE_MODEL_TOAST_TITLE = 'Model selected'; diff --git a/ui/desktop/src/components/settings_v2/models/AddModelButton.tsx b/ui/desktop/src/components/settings_v2/models/subcomponents/AddModelButton.tsx similarity index 79% rename from ui/desktop/src/components/settings_v2/models/AddModelButton.tsx rename to ui/desktop/src/components/settings_v2/models/subcomponents/AddModelButton.tsx index cfd5e899adfc..3d972635633d 100644 --- a/ui/desktop/src/components/settings_v2/models/AddModelButton.tsx +++ b/ui/desktop/src/components/settings_v2/models/subcomponents/AddModelButton.tsx @@ -1,7 +1,7 @@ import React, { useState } from 'react'; -import { Plus } from 'lucide-react'; -import { Button } from '../../ui/button'; +import { Button } from '../../../ui/button'; import { AddModelModal } from './AddModelModal'; +import { Gear } from '../../../icons'; export const AddModelButton = () => { const [isAddModelModalOpen, setIsAddModelModalOpen] = useState(false); @@ -12,8 +12,8 @@ export const AddModelButton = () => { 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)} > - - Add Model + + Switch Models {isAddModelModalOpen ? setIsAddModelModalOpen(false)} /> : null} diff --git a/ui/desktop/src/components/settings_v2/models/AddModelModal.tsx b/ui/desktop/src/components/settings_v2/models/subcomponents/AddModelModal.tsx similarity index 75% rename from ui/desktop/src/components/settings_v2/models/AddModelModal.tsx rename to ui/desktop/src/components/settings_v2/models/subcomponents/AddModelModal.tsx index f717f123920d..124e387d0340 100644 --- a/ui/desktop/src/components/settings_v2/models/AddModelModal.tsx +++ b/ui/desktop/src/components/settings_v2/models/subcomponents/AddModelModal.tsx @@ -1,14 +1,13 @@ 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'; +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'; const ModalButtons = ({ onSubmit, onCancel }) => (
@@ -35,25 +34,12 @@ type AddModelModalProps = { onClose: () => void }; export const AddModelModal = ({ onClose }: AddModelModalProps) => { const { getProviders, upsert } = useConfig(); const [providerOptions, setProviderOptions] = useState([]); + const [modelOptions, setModelOptions] = useState([]); const [provider, setProvider] = useState(null); - const [modelName, setModelName] = useState(''); + const [model, setModel] = useState(''); 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, - }); - } + await switchModel({ model: model, provider: provider, writeToConfig: upsert }); }; useEffect(() => { @@ -67,6 +53,13 @@ export const AddModelModal = ({ onClose }: AddModelModalProps) => { label: metadata.display_name, })) ); + setModelOptions( + activeProviders.map(({ metadata, name }) => ({ + value: name, + label: metadata.display_name, + options: metadata.known_models, + })) + ); } catch (error) { console.error('Failed to load providers:', error); } @@ -103,7 +96,7 @@ export const AddModelModal = ({ onClose }: AddModelModalProps) => { value={providerOptions.find((option) => option.value === provider) || null} onChange={(option) => { setProvider(option?.value || null); - setModelName(''); + setModel(''); }} placeholder="Provider" isClearable @@ -111,8 +104,8 @@ export const AddModelModal = ({ onClose }: AddModelModalProps) => { setModelName(event.target.value)} - value={modelName} + onChange={(event) => setModel(event.target.value)} + value={model} />
diff --git a/ui/desktop/src/components/settings_v2/models/subcomponents/ConfigureModelButtons.tsx b/ui/desktop/src/components/settings_v2/models/subcomponents/ConfigureModelButtons.tsx new file mode 100644 index 000000000000..179acb0e76ab --- /dev/null +++ b/ui/desktop/src/components/settings_v2/models/subcomponents/ConfigureModelButtons.tsx @@ -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 ConfigureModelButtons({ setView }: ConfigureModelButtonsProps) { + return ( +
+ + +
+ ); +} From c627c13c35abade6717f83ef926122058e279200 Mon Sep 17 00:00:00 2001 From: Lily Delalande Date: Tue, 25 Mar 2025 17:34:34 -0400 Subject: [PATCH 3/8] use model list from provider metadata --- .../models/subcomponents/AddModelModal.tsx | 90 ++++++++++++++++--- 1 file changed, 76 insertions(+), 14 deletions(-) diff --git a/ui/desktop/src/components/settings_v2/models/subcomponents/AddModelModal.tsx b/ui/desktop/src/components/settings_v2/models/subcomponents/AddModelModal.tsx index 124e387d0340..99a94eb5ff02 100644 --- a/ui/desktop/src/components/settings_v2/models/subcomponents/AddModelModal.tsx +++ b/ui/desktop/src/components/settings_v2/models/subcomponents/AddModelModal.tsx @@ -37,6 +37,7 @@ export const AddModelModal = ({ onClose }: AddModelModalProps) => { const [modelOptions, setModelOptions] = useState([]); const [provider, setProvider] = useState(null); const [model, setModel] = useState(''); + const [isCustomModel, setIsCustomModel] = useState(false); const changeModel = async () => { await switchModel({ model: model, provider: provider, writeToConfig: upsert }); @@ -53,19 +54,54 @@ export const AddModelModal = ({ onClose }: AddModelModalProps) => { label: metadata.display_name, })) ); - setModelOptions( - activeProviders.map(({ metadata, name }) => ({ - value: name, - label: metadata.display_name, - options: metadata.known_models, - })) - ); + + // 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: 'Enter other value', + 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 (
}> @@ -74,7 +110,7 @@ export const AddModelModal = ({ onClose }: AddModelModalProps) => {
Add model
- Configure your AI model providers by adding their API keys. your Keys are stored + Configure your AI model providers by adding their API keys. Your keys are stored securely and encrypted locally.
@@ -97,16 +133,42 @@ export const AddModelModal = ({ onClose }: AddModelModalProps) => { onChange={(option) => { setProvider(option?.value || null); setModel(''); + setIsCustomModel(false); }} placeholder="Provider" isClearable /> - setModel(event.target.value)} - value={model} - /> + + {provider && ( + <> + {!isCustomModel ? ( + setModel(event.target.value)} + value={model} + /> +
+ )} + + )}
From 297bdcb2975776e65ccf07b2661770290f160b82 Mon Sep 17 00:00:00 2001 From: Lily Delalande Date: Tue, 25 Mar 2025 18:40:36 -0400 Subject: [PATCH 4/8] fix bottom bar --- ui/desktop/src/components/BottomMenu.tsx | 70 +------------------ .../settings_v2/models/ModelsSection.tsx | 4 +- .../components/settings_v2/models/index.ts | 67 ++++++++++++++++-- .../models/subcomponents/BottomBar.tsx | 69 ++++++++++++++++++ .../subcomponents/CurrentModelProvider.tsx | 27 +++++++ ...elButtons.tsx => ModelSettingsButtons.tsx} | 2 +- 6 files changed, 163 insertions(+), 76 deletions(-) create mode 100644 ui/desktop/src/components/settings_v2/models/subcomponents/BottomBar.tsx create mode 100644 ui/desktop/src/components/settings_v2/models/subcomponents/CurrentModelProvider.tsx rename ui/desktop/src/components/settings_v2/models/subcomponents/{ConfigureModelButtons.tsx => ModelSettingsButtons.tsx} (89%) diff --git a/ui/desktop/src/components/BottomMenu.tsx b/ui/desktop/src/components/BottomMenu.tsx index 57e3ee5800b2..56cc49213b3b 100644 --- a/ui/desktop/src/components/BottomMenu.tsx +++ b/ui/desktop/src/components/BottomMenu.tsx @@ -1,11 +1,10 @@ import React, { useState, useEffect, useRef } from 'react'; import { useModel } from './settings/models/ModelContext'; import { useRecentModels } from './settings/models/RecentModels'; // Hook for recent models -import { Sliders } from 'lucide-react'; -import { ModelRadioList } from './settings/models/ModelRadioList'; -import { Document, ChevronUp, ChevronDown } from './icons'; +import { Document, ChevronUp } from './icons'; import type { View } from '../App'; import { BottomMenuModeSelection } from './BottomMenuModeSelection'; +import ModelsBottomBar from './settings_v2/models/subcomponents/BottomBar'; export default function BottomMenu({ hasMessages, @@ -77,70 +76,7 @@ export default function BottomMenu({ {/* Model Selector Dropdown - Only in development */} -
-
setIsModelMenuOpen(!isModelMenuOpen)} - > - {(currentModel?.alias ?? currentModel?.name) || 'Select Model'} - {isModelMenuOpen ? ( - - ) : ( - - )} -
- - {/* Dropdown Menu */} - {isModelMenuOpen && ( -
-
- ( - - )} - /> -
{ - setIsModelMenuOpen(false); - setView('settings'); - }} - > - Tools and Settings - -
-
-
- )} -
+ ); } diff --git a/ui/desktop/src/components/settings_v2/models/ModelsSection.tsx b/ui/desktop/src/components/settings_v2/models/ModelsSection.tsx index 47a5ea68f452..0e988e76a853 100644 --- a/ui/desktop/src/components/settings_v2/models/ModelsSection.tsx +++ b/ui/desktop/src/components/settings_v2/models/ModelsSection.tsx @@ -1,6 +1,6 @@ import React, { useEffect, useState } from 'react'; import type { View } from '../../../App'; -import ConfigureModelButtons from './subcomponents/ConfigureModelButtons'; +import ModelSettingsButtons from './subcomponents/ModelSettingsButtons'; import { useConfig } from '../../ConfigContext'; import { ToastError } from '../../settings/models/toasts'; @@ -54,7 +54,7 @@ export default function ModelsSection({ setView }: ModelsSectionProps) {

{model}

{provider}

- + ); diff --git a/ui/desktop/src/components/settings_v2/models/index.ts b/ui/desktop/src/components/settings_v2/models/index.ts index 018adadb8937..950a62056632 100644 --- a/ui/desktop/src/components/settings_v2/models/index.ts +++ b/ui/desktop/src/components/settings_v2/models/index.ts @@ -1,14 +1,17 @@ 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'; @@ -59,20 +62,17 @@ export async function changeModel({ model, provider, writeToConfig }: changeMode } interface startAgentFromConfigProps { - readFromConfig: (key: string, is_secret: boolean) => Promise; + readFromConfig: (key: string, is_secret: boolean) => Promise; } // starts agent with the values for GOOSE_PROVIDER and GOOSE_MODEL that are in the config export async function startAgentFromConfig({ readFromConfig }: startAgentFromConfigProps) { - let model: string; - let provider: string; + let modelProvider: { model: string; provider: string }; // read from config try { - model = (await readFromConfig('GOOSE_MODEL', false)) as string; - provider = (await readFromConfig('GOOSE_PROVIDER', false)) as string; + modelProvider = await getCurrentModelAndProvider({ readFromConfig: readFromConfig }); } catch (error) { - console.error(`Failed to read GOOSE_MODEL or GOOSE_PROVIDER from config`); // show toast with error ToastError({ title: START_AGENT_TITLE, @@ -82,6 +82,9 @@ export async function startAgentFromConfig({ readFromConfig }: startAgentFromCon return; } + const model = modelProvider.model; + const provider = modelProvider.provider; + console.log(`Starting agent with GOOSE_MODEL=${model} and GOOSE_PROVIDER=${provider}`); try { @@ -103,3 +106,55 @@ export async function startAgentFromConfig({ readFromConfig }: startAgentFromCon }); } } + +interface getCurrentModelAndProviderProps { + readFromConfig: (key: string, is_secret: boolean) => Promise; +} + +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; + getProviders: (b: boolean) => Promise; +} + +// 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 }; +} diff --git a/ui/desktop/src/components/settings_v2/models/subcomponents/BottomBar.tsx b/ui/desktop/src/components/settings_v2/models/subcomponents/BottomBar.tsx new file mode 100644 index 000000000000..6c48f5dd5ee7 --- /dev/null +++ b/ui/desktop/src/components/settings_v2/models/subcomponents/BottomBar.tsx @@ -0,0 +1,69 @@ +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'; + +interface ModelsBottomBarProps { + dropdownRef: any; + setView: any; +} +export default function ModelsBottomBar({ dropdownRef }: ModelsBottomBarProps) { + const { read, getProviders } = useConfig(); + const [isModelMenuOpen, setIsModelMenuOpen] = useState(false); + const [provider, setProvider] = useState(null); + const [model, setModel] = useState(''); + const [isAddModelModalOpen, setIsAddModelModalOpen] = useState(false); + + useEffect(() => { + (async () => { + const modelProvider = await getCurrentModelAndProviderForDisplay({ + readFromConfig: read, + getProviders, + }); + setProvider(modelProvider.provider); + setModel(modelProvider.model); + })(); + }, [read, getProviders]); + + return ( +
+
setIsModelMenuOpen(!isModelMenuOpen)} + > + {model} + {isModelMenuOpen ? ( + + ) : ( + + )} +
+ + {/* Dropdown Menu */} + {isModelMenuOpen && ( +
+
+
Current:
+
+ {model} -- {provider} +
+
{ + setIsModelMenuOpen(false); + setIsAddModelModalOpen(true); + }} + > + Change Model + +
+
+
+ )} + {isAddModelModalOpen ? setIsAddModelModalOpen(false)} /> : null} +
+ ); +} diff --git a/ui/desktop/src/components/settings_v2/models/subcomponents/CurrentModelProvider.tsx b/ui/desktop/src/components/settings_v2/models/subcomponents/CurrentModelProvider.tsx new file mode 100644 index 000000000000..24d61cab7a2d --- /dev/null +++ b/ui/desktop/src/components/settings_v2/models/subcomponents/CurrentModelProvider.tsx @@ -0,0 +1,27 @@ +import React, { useEffect, useState } from 'react'; +import { useConfig } from '../../../ConfigContext'; +import { getCurrentModelAndProviderForDisplay } from '@/src/components/settings_v2/models'; + +export default function CurrentModelProvider(arg) { + const [provider, setProvider] = useState(null); + const [model, setModel] = useState(''); + const { read, getProviders } = useConfig(); + + useEffect(() => { + (async () => { + const modelProvider = await getCurrentModelAndProviderForDisplay({ + readFromConfig: read, + getProviders, + }); + setProvider(modelProvider.provider); + setModel(modelProvider.model); + })(); + }, [read, getProviders]); + + return ( +
+

{model}

+

{provider}

+
+ ); +} diff --git a/ui/desktop/src/components/settings_v2/models/subcomponents/ConfigureModelButtons.tsx b/ui/desktop/src/components/settings_v2/models/subcomponents/ModelSettingsButtons.tsx similarity index 89% rename from ui/desktop/src/components/settings_v2/models/subcomponents/ConfigureModelButtons.tsx rename to ui/desktop/src/components/settings_v2/models/subcomponents/ModelSettingsButtons.tsx index 179acb0e76ab..08b77bc7942f 100644 --- a/ui/desktop/src/components/settings_v2/models/subcomponents/ConfigureModelButtons.tsx +++ b/ui/desktop/src/components/settings_v2/models/subcomponents/ModelSettingsButtons.tsx @@ -8,7 +8,7 @@ interface ConfigureModelButtonsProps { setView: (view: View) => void; } -export default function ConfigureModelButtons({ setView }: ConfigureModelButtonsProps) { +export default function ModelSettingsButtons({ setView }: ConfigureModelButtonsProps) { return (
From 3eee8b52155320b04f600e5f82a4c9ee69ac7a0a Mon Sep 17 00:00:00 2001 From: Lily Delalande Date: Tue, 25 Mar 2025 18:52:33 -0400 Subject: [PATCH 5/8] allow to configure provider from modal --- ui/desktop/src/components/BottomMenu.tsx | 4 +- .../models/subcomponents/AddModelButton.tsx | 11 +++++- .../models/subcomponents/AddModelModal.tsx | 39 +++++++++++++------ .../subcomponents/ModelSettingsButtons.tsx | 2 +- .../{BottomBar.tsx => ModelsBottomBar.tsx} | 9 +++-- 5 files changed, 45 insertions(+), 20 deletions(-) rename ui/desktop/src/components/settings_v2/models/subcomponents/{BottomBar.tsx => ModelsBottomBar.tsx} (88%) diff --git a/ui/desktop/src/components/BottomMenu.tsx b/ui/desktop/src/components/BottomMenu.tsx index 56cc49213b3b..062c09175d89 100644 --- a/ui/desktop/src/components/BottomMenu.tsx +++ b/ui/desktop/src/components/BottomMenu.tsx @@ -4,7 +4,7 @@ import { useRecentModels } from './settings/models/RecentModels'; // Hook for re import { Document, ChevronUp } from './icons'; import type { View } from '../App'; import { BottomMenuModeSelection } from './BottomMenuModeSelection'; -import ModelsBottomBar from './settings_v2/models/subcomponents/BottomBar'; +import ModelsBottomBar from './settings_v2/models/subcomponents/ModelsBottomBar'; export default function BottomMenu({ hasMessages, @@ -76,7 +76,7 @@ export default function BottomMenu({ {/* Model Selector Dropdown - Only in development */} - +
); } diff --git a/ui/desktop/src/components/settings_v2/models/subcomponents/AddModelButton.tsx b/ui/desktop/src/components/settings_v2/models/subcomponents/AddModelButton.tsx index 3d972635633d..149daf0e1517 100644 --- a/ui/desktop/src/components/settings_v2/models/subcomponents/AddModelButton.tsx +++ b/ui/desktop/src/components/settings_v2/models/subcomponents/AddModelButton.tsx @@ -2,8 +2,13 @@ import React, { useState } from 'react'; import { Button } from '../../../ui/button'; import { AddModelModal } from './AddModelModal'; import { Gear } from '../../../icons'; +import type { View } from '../../../../App'; -export const AddModelButton = () => { +interface AddModelButtonProps { + setView: (view: View) => void; +} + +export const AddModelButton = ({ setView }: AddModelButtonProps) => { const [isAddModelModalOpen, setIsAddModelModalOpen] = useState(false); return ( @@ -15,7 +20,9 @@ export const AddModelButton = () => { Switch Models - {isAddModelModalOpen ? setIsAddModelModalOpen(false)} /> : null} + {isAddModelModalOpen ? ( + setIsAddModelModalOpen(false)} /> + ) : null} ); }; diff --git a/ui/desktop/src/components/settings_v2/models/subcomponents/AddModelModal.tsx b/ui/desktop/src/components/settings_v2/models/subcomponents/AddModelModal.tsx index 99a94eb5ff02..b26cadf3ccfe 100644 --- a/ui/desktop/src/components/settings_v2/models/subcomponents/AddModelModal.tsx +++ b/ui/desktop/src/components/settings_v2/models/subcomponents/AddModelModal.tsx @@ -8,6 +8,7 @@ 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 }) => (
@@ -30,8 +31,11 @@ const ModalButtons = ({ onSubmit, onCancel }) => (
); -type AddModelModalProps = { onClose: () => void }; -export const AddModelModal = ({ onClose }: AddModelModalProps) => { +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([]); @@ -48,12 +52,17 @@ export const AddModelModal = ({ onClose }: AddModelModalProps) => { try { const providersResponse = await getProviders(false); const activeProviders = providersResponse.filter((provider) => provider.is_configured); - setProviderOptions( - activeProviders.map(({ metadata, name }) => ({ + // 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 = []; @@ -74,7 +83,7 @@ export const AddModelModal = ({ onClose }: AddModelModalProps) => { formattedModelOptions.forEach((group) => { group.options.push({ value: 'custom', - label: 'Enter other value', + label: 'Use custom model', provider: group.options[0]?.provider, }); }); @@ -131,9 +140,15 @@ export const AddModelModal = ({ onClose }: AddModelModalProps) => { options={providerOptions} value={providerOptions.find((option) => option.value === provider) || null} onChange={(option) => { - setProvider(option?.value || null); - setModel(''); - setIsCustomModel(false); + 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 @@ -151,10 +166,10 @@ export const AddModelModal = ({ onClose }: AddModelModalProps) => { ) : (
- + diff --git a/ui/desktop/src/components/settings_v2/models/subcomponents/ModelSettingsButtons.tsx b/ui/desktop/src/components/settings_v2/models/subcomponents/ModelSettingsButtons.tsx index 08b77bc7942f..ab3ae3da3562 100644 --- a/ui/desktop/src/components/settings_v2/models/subcomponents/ModelSettingsButtons.tsx +++ b/ui/desktop/src/components/settings_v2/models/subcomponents/ModelSettingsButtons.tsx @@ -11,7 +11,7 @@ interface ConfigureModelButtonsProps { export default function ModelSettingsButtons({ setView }: ConfigureModelButtonsProps) { return (
- +
)} - {isAddModelModalOpen ? setIsAddModelModalOpen(false)} /> : null} + {isAddModelModalOpen ? ( + setIsAddModelModalOpen(false)} /> + ) : null}
); } From 448fe332185b02959f7ed7688270c6e7ef3f7468 Mon Sep 17 00:00:00 2001 From: Lily Delalande Date: Tue, 25 Mar 2025 20:52:53 -0400 Subject: [PATCH 6/8] update bottom bar --- ui/desktop/src/components/BottomMenu.tsx | 77 +++++++++++++++++++++++- 1 file changed, 74 insertions(+), 3 deletions(-) diff --git a/ui/desktop/src/components/BottomMenu.tsx b/ui/desktop/src/components/BottomMenu.tsx index 062c09175d89..6358d7411cfb 100644 --- a/ui/desktop/src/components/BottomMenu.tsx +++ b/ui/desktop/src/components/BottomMenu.tsx @@ -1,7 +1,9 @@ import React, { useState, useEffect, useRef } from 'react'; import { useModel } from './settings/models/ModelContext'; import { useRecentModels } from './settings/models/RecentModels'; // Hook for recent models -import { Document, ChevronUp } from './icons'; +import { Sliders } from 'lucide-react'; +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'; @@ -75,8 +77,77 @@ export default function BottomMenu({ {/* Goose Mode Selector Dropdown */} - {/* Model Selector Dropdown - Only in development */} - + {/* Model Selector Dropdown */} + {process.env.ALPHA === 'true' ? ( + + ) : ( +
+
setIsModelMenuOpen(!isModelMenuOpen)} + > + {(currentModel?.alias ?? currentModel?.name) || 'Select Model'} + {isModelMenuOpen ? ( + + ) : ( + + )} +
+ + {/* Dropdown Menu */} + {isModelMenuOpen && ( +
+
+ ( + + )} + /> +
{ + setIsModelMenuOpen(false); + setView('settings'); + }} + > + Tools and Settings + +
+
+
+ )} +
+ )} ); } From 308c43e4d887ae24b059e76625e053c3681244f1 Mon Sep 17 00:00:00 2001 From: Lily Delalande Date: Tue, 25 Mar 2025 20:54:35 -0400 Subject: [PATCH 7/8] dont bork bottom menu --- ui/desktop/src/components/BottomMenu.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/desktop/src/components/BottomMenu.tsx b/ui/desktop/src/components/BottomMenu.tsx index 6358d7411cfb..a9e3134b9209 100644 --- a/ui/desktop/src/components/BottomMenu.tsx +++ b/ui/desktop/src/components/BottomMenu.tsx @@ -78,7 +78,7 @@ export default function BottomMenu({ {/* Model Selector Dropdown */} - {process.env.ALPHA === 'true' ? ( + {process.env.ALPHA ? ( ) : (
From ea4bc05e0701d5fb6ec968c62c811b97f164d4a8 Mon Sep 17 00:00:00 2001 From: Lily Delalande Date: Tue, 25 Mar 2025 20:56:00 -0400 Subject: [PATCH 8/8] remove unused file --- .../subcomponents/CurrentModelProvider.tsx | 27 ------------------- 1 file changed, 27 deletions(-) delete mode 100644 ui/desktop/src/components/settings_v2/models/subcomponents/CurrentModelProvider.tsx diff --git a/ui/desktop/src/components/settings_v2/models/subcomponents/CurrentModelProvider.tsx b/ui/desktop/src/components/settings_v2/models/subcomponents/CurrentModelProvider.tsx deleted file mode 100644 index 24d61cab7a2d..000000000000 --- a/ui/desktop/src/components/settings_v2/models/subcomponents/CurrentModelProvider.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import React, { useEffect, useState } from 'react'; -import { useConfig } from '../../../ConfigContext'; -import { getCurrentModelAndProviderForDisplay } from '@/src/components/settings_v2/models'; - -export default function CurrentModelProvider(arg) { - const [provider, setProvider] = useState(null); - const [model, setModel] = useState(''); - const { read, getProviders } = useConfig(); - - useEffect(() => { - (async () => { - const modelProvider = await getCurrentModelAndProviderForDisplay({ - readFromConfig: read, - getProviders, - }); - setProvider(modelProvider.provider); - setModel(modelProvider.model); - })(); - }, [read, getProviders]); - - return ( -
-

{model}

-

{provider}

-
- ); -}