Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 8 additions & 25 deletions ui/desktop/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,22 +14,20 @@ import { type Recipe } from './recipe';

import ChatView from './components/ChatView';
import SuspenseLoader from './suspense-loader';
import { type SettingsViewOptions } from './components/settings/SettingsView';
import SettingsViewV2 from './components/settings_v2/SettingsView';
import MoreModelsView from './components/settings/models/MoreModelsView';
import ConfigureProvidersView from './components/settings/providers/ConfigureProvidersView';
import SettingsView, { SettingsViewOptions } from './components/settings/SettingsView';
import SessionsView from './components/sessions/SessionsView';
import SharedSessionView from './components/sessions/SharedSessionView';
import SchedulesView from './components/schedule/SchedulesView';
import ProviderSettings from './components/settings_v2/providers/ProviderSettingsPage';
import ProviderSettings from './components/settings/providers/ProviderSettingsPage';
import RecipeEditor from './components/RecipeEditor';
import { useChat } from './hooks/useChat';

import 'react-toastify/dist/ReactToastify.css';
import { useConfig, MalformedConfigError } from './components/ConfigContext';
import { addExtensionFromDeepLink as addExtensionFromDeepLinkV2 } from './components/settings_v2/extensions';
import { ModelAndProviderProvider } from './components/ModelAndProviderContext';
import { addExtensionFromDeepLink as addExtensionFromDeepLinkV2 } from './components/settings/extensions';
import { backupConfig, initConfig, readAllConfig } from './api/sdk.gen';
import PermissionSettingsView from './components/settings_v2/permission/PermissionSetting';
import PermissionSettingsView from './components/settings/permission/PermissionSetting';

import { type SessionDetails } from './sessions';

Expand Down Expand Up @@ -462,7 +460,7 @@ export default function App() {
);

return (
<>
<ModelAndProviderProvider>
<ToastContainer
aria-label="Toast notifications"
toastClassName={() =>
Expand Down Expand Up @@ -496,29 +494,14 @@ export default function App() {
<ProviderSettings onClose={() => setView('chat')} isOnboarding={true} />
)}
{view === 'settings' && (
<SettingsViewV2
<SettingsView
onClose={() => {
setView('chat');
}}
setView={setView}
viewOptions={viewOptions as SettingsViewOptions}
/>
)}
{view === 'moreModels' && (
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we not need these anymore?

<MoreModelsView
onClose={() => {
setView('settings');
}}
setView={setView}
/>
)}
{view === 'configureProviders' && (
<ConfigureProvidersView
onClose={() => {
setView('settings');
}}
/>
)}
{view === 'ConfigureProviders' && (
<ProviderSettings onClose={() => setView('chat')} isOnboarding={false} />
)}
Expand Down Expand Up @@ -579,6 +562,6 @@ export default function App() {
setIsGoosehintsModalOpen={setIsGoosehintsModalOpen}
/>
)}
</>
</ModelAndProviderProvider>
);
}
2 changes: 1 addition & 1 deletion ui/desktop/src/components/ConfigContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import type {
ExtensionQuery,
ExtensionConfig,
} from '../api/types.gen';
import { removeShims } from './settings_v2/extensions/utils';
import { removeShims } from './settings/extensions/utils';

export type { ExtensionConfig } from '../api/types.gen';

Expand Down
190 changes: 190 additions & 0 deletions ui/desktop/src/components/ModelAndProviderContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
import React, { createContext, useContext, useState, useEffect, useMemo, useCallback } from 'react';
import { initializeAgent } from '../agent';
import { toastError, toastSuccess } from '../toasts';
import Model, { getProviderMetadata } from './settings/models/modelInterface';
import { ProviderMetadata } from '../api';
import { useConfig } from './ConfigContext';

// titles
export const UNKNOWN_PROVIDER_TITLE = 'Provider name lookup';

// errors
const CHANGE_MODEL_ERROR_TITLE = 'Change failed';
const SWITCH_MODEL_AGENT_ERROR_MSG =
'Failed to start agent with selected model -- please try again';
const CONFIG_UPDATE_ERROR_MSG = 'Failed to update configuration settings -- please try again';
export const UNKNOWN_PROVIDER_MSG = 'Unknown provider in config -- please inspect your config.yaml';

// success
const CHANGE_MODEL_TOAST_TITLE = 'Model changed';
const SWITCH_MODEL_SUCCESS_MSG = 'Successfully switched models';

interface ModelAndProviderContextType {
currentModel: string | null;
currentProvider: string | null;
changeModel: (model: Model) => Promise<void>;
getCurrentModelAndProvider: () => Promise<{ model: string; provider: string }>;
getFallbackModelAndProvider: () => Promise<{ model: string; provider: string }>;
getCurrentModelAndProviderForDisplay: () => Promise<{ model: string; provider: string }>;
refreshCurrentModelAndProvider: () => Promise<void>;
}

interface ModelAndProviderProviderProps {
children: React.ReactNode;
}

const ModelAndProviderContext = createContext<ModelAndProviderContextType | undefined>(undefined);

export const ModelAndProviderProvider: React.FC<ModelAndProviderProviderProps> = ({ children }) => {
const [currentModel, setCurrentModel] = useState<string | null>(null);
const [currentProvider, setCurrentProvider] = useState<string | null>(null);
const { read, upsert, getProviders } = useConfig();

const changeModel = useCallback(
async (model: Model) => {
const modelName = model.name;
const providerName = model.provider;
try {
await initializeAgent({
model: model.name,
provider: model.provider,
});
} catch (error) {
console.error(`Failed to change model at agent step -- ${modelName} ${providerName}`);
toastError({
title: CHANGE_MODEL_ERROR_TITLE,
msg: SWITCH_MODEL_AGENT_ERROR_MSG,
traceback: error instanceof Error ? error.message : String(error),
});
// don't write to config
return;
}

try {
await upsert('GOOSE_PROVIDER', providerName, false);
await upsert('GOOSE_MODEL', modelName, false);

// Update local state
setCurrentProvider(providerName);
setCurrentModel(modelName);
} catch (error) {
console.error(`Failed to change model at config step -- ${modelName} ${providerName}}`);
toastError({
title: CHANGE_MODEL_ERROR_TITLE,
msg: CONFIG_UPDATE_ERROR_MSG,
traceback: error instanceof Error ? error.message : String(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.alias ?? modelName} from ${model.subtext ?? providerName}`,
});
}
},
[upsert]
);

const getFallbackModelAndProvider = useCallback(async () => {
const provider = window.appConfig.get('GOOSE_DEFAULT_PROVIDER') as string;
const model = window.appConfig.get('GOOSE_DEFAULT_MODEL') as string;
if (provider && model) {
try {
await upsert('GOOSE_MODEL', model, false);
await upsert('GOOSE_PROVIDER', provider, false);
} catch (error) {
console.error('[getFallbackModelAndProvider] Failed to write to config', error);
}
}
return { model: model, provider: provider };
}, [upsert]);

const getCurrentModelAndProvider = useCallback(async () => {
let model: string;
let provider: string;

// read from config
try {
model = (await read('GOOSE_MODEL', false)) as string;
provider = (await read('GOOSE_PROVIDER', false)) as string;
} catch (error) {
console.error(`Failed to read GOOSE_MODEL or GOOSE_PROVIDER from config`);
throw error;
}
if (!model || !provider) {
console.log('[getCurrentModelAndProvider] Checking app environment as fallback');
return getFallbackModelAndProvider();
}
return { model: model, provider: provider };
}, [read, getFallbackModelAndProvider]);

const getCurrentModelAndProviderForDisplay = useCallback(async () => {
const modelProvider = await getCurrentModelAndProvider();
const gooseModel = modelProvider.model;
const gooseProvider = modelProvider.provider;

// lookup display name
let metadata: ProviderMetadata;

try {
metadata = await getProviderMetadata(String(gooseProvider), getProviders);
} catch (error) {
return { model: gooseModel, provider: gooseProvider };
}
const providerDisplayName = metadata.display_name;

return { model: gooseModel, provider: providerDisplayName };
}, [getCurrentModelAndProvider, getProviders]);

const refreshCurrentModelAndProvider = useCallback(async () => {
try {
const { model, provider } = await getCurrentModelAndProvider();
setCurrentModel(model);
setCurrentProvider(provider);
} catch (error) {
console.error('Failed to refresh current model and provider:', error);
}
}, [getCurrentModelAndProvider]);

// Load initial model and provider on mount
useEffect(() => {
refreshCurrentModelAndProvider();
}, [refreshCurrentModelAndProvider]);

const contextValue = useMemo(
() => ({
currentModel,
currentProvider,
changeModel,
getCurrentModelAndProvider,
getFallbackModelAndProvider,
getCurrentModelAndProviderForDisplay,
refreshCurrentModelAndProvider,
}),
[
currentModel,
currentProvider,
changeModel,
getCurrentModelAndProvider,
getFallbackModelAndProvider,
getCurrentModelAndProviderForDisplay,
refreshCurrentModelAndProvider,
]
);

return (
<ModelAndProviderContext.Provider value={contextValue}>
{children}
</ModelAndProviderContext.Provider>
);
};

export const useModelAndProvider = () => {
const context = useContext(ModelAndProviderContext);
if (context === undefined) {
throw new Error('useModelAndProvider must be used within a ModelAndProviderProvider');
}
return context;
};
Loading
Loading