alexhancock/remove-settings-v1 (#2744)
Co-authored-by: Zane Staggs <zane@squareup.com>
@@ -14,22 +14,20 @@ import { type Recipe } from './recipe';
|
|||||||
|
|
||||||
import ChatView from './components/ChatView';
|
import ChatView from './components/ChatView';
|
||||||
import SuspenseLoader from './suspense-loader';
|
import SuspenseLoader from './suspense-loader';
|
||||||
import { type SettingsViewOptions } from './components/settings/SettingsView';
|
import SettingsView, { 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 SessionsView from './components/sessions/SessionsView';
|
import SessionsView from './components/sessions/SessionsView';
|
||||||
import SharedSessionView from './components/sessions/SharedSessionView';
|
import SharedSessionView from './components/sessions/SharedSessionView';
|
||||||
import SchedulesView from './components/schedule/SchedulesView';
|
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 RecipeEditor from './components/RecipeEditor';
|
||||||
import { useChat } from './hooks/useChat';
|
import { useChat } from './hooks/useChat';
|
||||||
|
|
||||||
import 'react-toastify/dist/ReactToastify.css';
|
import 'react-toastify/dist/ReactToastify.css';
|
||||||
import { useConfig, MalformedConfigError } from './components/ConfigContext';
|
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 { 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';
|
import { type SessionDetails } from './sessions';
|
||||||
|
|
||||||
@@ -462,7 +460,7 @@ export default function App() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<ModelAndProviderProvider>
|
||||||
<ToastContainer
|
<ToastContainer
|
||||||
aria-label="Toast notifications"
|
aria-label="Toast notifications"
|
||||||
toastClassName={() =>
|
toastClassName={() =>
|
||||||
@@ -496,7 +494,7 @@ export default function App() {
|
|||||||
<ProviderSettings onClose={() => setView('chat')} isOnboarding={true} />
|
<ProviderSettings onClose={() => setView('chat')} isOnboarding={true} />
|
||||||
)}
|
)}
|
||||||
{view === 'settings' && (
|
{view === 'settings' && (
|
||||||
<SettingsViewV2
|
<SettingsView
|
||||||
onClose={() => {
|
onClose={() => {
|
||||||
setView('chat');
|
setView('chat');
|
||||||
}}
|
}}
|
||||||
@@ -504,21 +502,6 @@ export default function App() {
|
|||||||
viewOptions={viewOptions as SettingsViewOptions}
|
viewOptions={viewOptions as SettingsViewOptions}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{view === 'moreModels' && (
|
|
||||||
<MoreModelsView
|
|
||||||
onClose={() => {
|
|
||||||
setView('settings');
|
|
||||||
}}
|
|
||||||
setView={setView}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{view === 'configureProviders' && (
|
|
||||||
<ConfigureProvidersView
|
|
||||||
onClose={() => {
|
|
||||||
setView('settings');
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{view === 'ConfigureProviders' && (
|
{view === 'ConfigureProviders' && (
|
||||||
<ProviderSettings onClose={() => setView('chat')} isOnboarding={false} />
|
<ProviderSettings onClose={() => setView('chat')} isOnboarding={false} />
|
||||||
)}
|
)}
|
||||||
@@ -579,6 +562,6 @@ export default function App() {
|
|||||||
setIsGoosehintsModalOpen={setIsGoosehintsModalOpen}
|
setIsGoosehintsModalOpen={setIsGoosehintsModalOpen}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</>
|
</ModelAndProviderProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import type {
|
|||||||
ExtensionQuery,
|
ExtensionQuery,
|
||||||
ExtensionConfig,
|
ExtensionConfig,
|
||||||
} from '../api/types.gen';
|
} from '../api/types.gen';
|
||||||
import { removeShims } from './settings_v2/extensions/utils';
|
import { removeShims } from './settings/extensions/utils';
|
||||||
|
|
||||||
export type { ExtensionConfig } from '../api/types.gen';
|
export type { ExtensionConfig } from '../api/types.gen';
|
||||||
|
|
||||||
|
|||||||
190
ui/desktop/src/components/ModelAndProviderContext.tsx
Normal file
@@ -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;
|
||||||
|
};
|
||||||
@@ -1,215 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import {
|
|
||||||
supported_providers,
|
|
||||||
required_keys,
|
|
||||||
provider_aliases,
|
|
||||||
} from './settings/models/hardcoded_stuff';
|
|
||||||
import { useActiveKeys } from './settings/api_keys/ActiveKeysContext';
|
|
||||||
import { ProviderSetupModal } from './settings/ProviderSetupModal';
|
|
||||||
import { useModel } from './settings/models/ModelContext';
|
|
||||||
import { useRecentModels } from './settings/models/RecentModels';
|
|
||||||
import { createSelectedModel } from './settings/models/utils';
|
|
||||||
import { getDefaultModel } from './settings/models/hardcoded_stuff';
|
|
||||||
import { initializeSystem } from '../utils/providerUtils';
|
|
||||||
import { getApiUrl, getSecretKey } from '../config';
|
|
||||||
import { getActiveProviders, isSecretKey } from './settings/api_keys/utils';
|
|
||||||
import { BaseProviderGrid, getProviderDescription } from './settings/providers/BaseProviderGrid';
|
|
||||||
import { toastError, toastSuccess } from '../toasts';
|
|
||||||
|
|
||||||
interface ProviderGridProps {
|
|
||||||
onSubmit?: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ProviderGrid({ onSubmit }: ProviderGridProps) {
|
|
||||||
const { activeKeys, setActiveKeys } = useActiveKeys();
|
|
||||||
const [selectedId, setSelectedId] = React.useState<string | null>(null);
|
|
||||||
const [showSetupModal, setShowSetupModal] = React.useState(false);
|
|
||||||
const { switchModel } = useModel();
|
|
||||||
const { addRecentModel } = useRecentModels();
|
|
||||||
|
|
||||||
const providers = React.useMemo(() => {
|
|
||||||
return supported_providers.map((providerName) => {
|
|
||||||
const alias =
|
|
||||||
provider_aliases.find((p) => p.provider === providerName)?.alias ||
|
|
||||||
providerName.toLowerCase();
|
|
||||||
const isConfigured = activeKeys.includes(providerName);
|
|
||||||
|
|
||||||
return {
|
|
||||||
id: alias,
|
|
||||||
name: providerName,
|
|
||||||
isConfigured,
|
|
||||||
description: getProviderDescription(providerName),
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}, [activeKeys]);
|
|
||||||
|
|
||||||
const handleConfigure = async (provider: {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
isConfigured: boolean;
|
|
||||||
description: string;
|
|
||||||
}) => {
|
|
||||||
const providerId = provider.id.toLowerCase();
|
|
||||||
|
|
||||||
const modelName = getDefaultModel(providerId) || 'default-model';
|
|
||||||
const model = createSelectedModel(providerId, modelName);
|
|
||||||
|
|
||||||
await initializeSystem(providerId, model.name);
|
|
||||||
|
|
||||||
switchModel(model);
|
|
||||||
addRecentModel(model);
|
|
||||||
localStorage.setItem('GOOSE_PROVIDER', providerId);
|
|
||||||
|
|
||||||
toastSuccess({
|
|
||||||
title: provider.name,
|
|
||||||
msg: `Starting Goose with default model: ${getDefaultModel(provider.name.toLowerCase().replace(/ /g, '_'))}.`,
|
|
||||||
});
|
|
||||||
|
|
||||||
onSubmit?.();
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleAddKeys = (provider: {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
isConfigured: boolean;
|
|
||||||
description: string;
|
|
||||||
}) => {
|
|
||||||
setSelectedId(provider.id);
|
|
||||||
setShowSetupModal(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleModalSubmit = async (configValues: { [key: string]: string }) => {
|
|
||||||
if (!selectedId) return;
|
|
||||||
|
|
||||||
const provider = providers.find((p) => p.id === selectedId)?.name;
|
|
||||||
if (!provider) return;
|
|
||||||
|
|
||||||
const requiredKeys = required_keys[provider as keyof typeof required_keys];
|
|
||||||
if (!requiredKeys || requiredKeys.length === 0) {
|
|
||||||
console.error(`No keys found for provider ${provider}`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Delete existing keys if provider is already configured
|
|
||||||
const isUpdate = providers.find((p) => p.id === selectedId)?.isConfigured;
|
|
||||||
if (isUpdate) {
|
|
||||||
for (const keyName of requiredKeys) {
|
|
||||||
const isSecret = isSecretKey(keyName);
|
|
||||||
const deleteResponse = await fetch(getApiUrl('/configs/delete'), {
|
|
||||||
method: 'DELETE',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'X-Secret-Key': getSecretKey(),
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
key: keyName,
|
|
||||||
isSecret,
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!deleteResponse.ok) {
|
|
||||||
const errorText = await deleteResponse.text();
|
|
||||||
console.error('Delete response error:', errorText);
|
|
||||||
throw new Error(`Failed to delete old key: ${keyName}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Store new keys
|
|
||||||
for (const keyName of requiredKeys) {
|
|
||||||
const value = configValues[keyName];
|
|
||||||
if (!value) {
|
|
||||||
console.error(`Missing value for required key: ${keyName}`);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const isSecret = isSecretKey(keyName);
|
|
||||||
const storeResponse = await fetch(getApiUrl('/configs/store'), {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'X-Secret-Key': getSecretKey(),
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
key: keyName,
|
|
||||||
value: value,
|
|
||||||
isSecret,
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!storeResponse.ok) {
|
|
||||||
const errorText = await storeResponse.text();
|
|
||||||
console.error('Store response error:', errorText);
|
|
||||||
throw new Error(`Failed to store new key: ${keyName}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
toastSuccess({
|
|
||||||
title: provider,
|
|
||||||
msg: isUpdate ? `Successfully updated configuration` : `Successfully added configuration`,
|
|
||||||
});
|
|
||||||
|
|
||||||
const updatedKeys = await getActiveProviders();
|
|
||||||
setActiveKeys(updatedKeys);
|
|
||||||
|
|
||||||
setShowSetupModal(false);
|
|
||||||
setSelectedId(null);
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Error handling modal submit:', err);
|
|
||||||
const errorMessage = err instanceof Error ? err.message : 'Unknown error occurred';
|
|
||||||
toastError({
|
|
||||||
title: provider,
|
|
||||||
msg: `Failed to ${providers.find((p) => p.id === selectedId)?.isConfigured ? 'update' : 'add'} configuration`,
|
|
||||||
traceback: errorMessage,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSelect = (providerId: string) => {
|
|
||||||
setSelectedId(selectedId === providerId ? null : providerId);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Add useEffect for Esc key handling
|
|
||||||
React.useEffect(() => {
|
|
||||||
const handleEsc = (event: KeyboardEvent) => {
|
|
||||||
if (event.key === 'Escape') {
|
|
||||||
setSelectedId(null);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
window.addEventListener('keydown', handleEsc);
|
|
||||||
return () => {
|
|
||||||
window.removeEventListener('keydown', handleEsc);
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="space-y-4 max-w-[1400px] mx-auto">
|
|
||||||
<BaseProviderGrid
|
|
||||||
providers={providers}
|
|
||||||
isSelectable={true}
|
|
||||||
selectedId={selectedId}
|
|
||||||
onSelect={handleSelect}
|
|
||||||
onAddKeys={handleAddKeys}
|
|
||||||
onTakeoff={(provider) => {
|
|
||||||
handleConfigure(provider);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{showSetupModal && selectedId && (
|
|
||||||
<div className="relative z-[9999]">
|
|
||||||
<ProviderSetupModal
|
|
||||||
provider={providers.find((p) => p.id === selectedId)?.name || 'Unknown Provider'}
|
|
||||||
_model="Example Model"
|
|
||||||
_endpoint="Example Endpoint"
|
|
||||||
onSubmit={handleModalSubmit}
|
|
||||||
onCancel={() => {
|
|
||||||
setShowSetupModal(false);
|
|
||||||
setSelectedId(null);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -11,7 +11,6 @@ import RecipeActivityEditor from './RecipeActivityEditor';
|
|||||||
import RecipeInfoModal from './RecipeInfoModal';
|
import RecipeInfoModal from './RecipeInfoModal';
|
||||||
import RecipeExpandableInfo from './RecipeExpandableInfo';
|
import RecipeExpandableInfo from './RecipeExpandableInfo';
|
||||||
import { ScheduleFromRecipeModal } from './schedule/ScheduleFromRecipeModal';
|
import { ScheduleFromRecipeModal } from './schedule/ScheduleFromRecipeModal';
|
||||||
// import ExtensionList from './settings_v2/extensions/subcomponents/ExtensionList';
|
|
||||||
|
|
||||||
interface RecipeEditorProps {
|
interface RecipeEditorProps {
|
||||||
config?: Recipe;
|
config?: Recipe;
|
||||||
@@ -365,7 +364,14 @@ export default function RecipeEditor({ config }: RecipeEditorProps) {
|
|||||||
recipe={getCurrentConfig()}
|
recipe={getCurrentConfig()}
|
||||||
onCreateSchedule={(deepLink) => {
|
onCreateSchedule={(deepLink) => {
|
||||||
// Open the schedules view with the deep link pre-filled
|
// Open the schedules view with the deep link pre-filled
|
||||||
window.electron.createChatWindow(undefined, undefined, undefined, undefined, undefined, 'schedules');
|
window.electron.createChatWindow(
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
'schedules'
|
||||||
|
);
|
||||||
// Store the deep link in localStorage for the schedules view to pick up
|
// Store the deep link in localStorage for the schedules view to pick up
|
||||||
localStorage.setItem('pendingScheduleDeepLink', deepLink);
|
localStorage.setItem('pendingScheduleDeepLink', deepLink);
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { snakeToTitleCase } from '../utils';
|
import { snakeToTitleCase } from '../utils';
|
||||||
import PermissionModal from './settings_v2/permission/PermissionModal';
|
import PermissionModal from './settings/permission/PermissionModal';
|
||||||
import { ChevronRight } from 'lucide-react';
|
import { ChevronRight } from 'lucide-react';
|
||||||
import { confirmPermission } from '../api';
|
import { confirmPermission } from '../api';
|
||||||
|
|
||||||
|
|||||||
@@ -1,86 +0,0 @@
|
|||||||
import { ProviderGrid } from './ProviderGrid';
|
|
||||||
import { ScrollArea } from './ui/scroll-area';
|
|
||||||
import { Button } from './ui/button';
|
|
||||||
import WelcomeGooseLogo from './WelcomeGooseLogo';
|
|
||||||
import MoreMenuLayout from './more_menu/MoreMenuLayout';
|
|
||||||
|
|
||||||
// Extending React CSSProperties to include custom webkit property
|
|
||||||
declare module 'react' {
|
|
||||||
interface CSSProperties {
|
|
||||||
WebkitAppRegion?: string;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
interface WelcomeScreenProps {
|
|
||||||
onSubmit?: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function WelcomeScreen({ onSubmit }: WelcomeScreenProps) {
|
|
||||||
return (
|
|
||||||
<div className="h-screen w-full select-none bg-white dark:bg-black">
|
|
||||||
<MoreMenuLayout showMenu={false} />
|
|
||||||
|
|
||||||
{/* Content area - explicitly set as non-draggable */}
|
|
||||||
<div
|
|
||||||
className="h-[calc(100vh-36px)] w-full overflow-hidden"
|
|
||||||
style={{ WebkitAppRegion: 'no-drag' }}
|
|
||||||
>
|
|
||||||
<ScrollArea className="h-full w-full">
|
|
||||||
<div className="flex min-h-full flex-col justify-center px-8 py-8 md:px-16 max-w-4xl mx-auto">
|
|
||||||
{/* Header Section */}
|
|
||||||
<div className="mb-8 mt-4">
|
|
||||||
<div className="flex items-center gap-4 mb-4">
|
|
||||||
<div className="group/logo">
|
|
||||||
<WelcomeGooseLogo className="h-16 w-16 md:h-20 md:w-20 text-black dark:text-white" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h1 className="text-4xl font-medium text-textStandard tracking-tight md:text-5xl">
|
|
||||||
Welcome to goose
|
|
||||||
</h1>
|
|
||||||
<p className="text-lg text-textSubtle max-w-2xl">
|
|
||||||
Your intelligent AI assistant for seamless productivity and creativity.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* ProviderGrid */}
|
|
||||||
<div className="w-full">
|
|
||||||
<h2
|
|
||||||
className="text-3xl font-medium text-textStandard tracking-tight mb-2"
|
|
||||||
data-testid="provider-selection-heading"
|
|
||||||
>
|
|
||||||
Choose a Provider
|
|
||||||
</h2>
|
|
||||||
<p className="text-xl text-textStandard mb-4">
|
|
||||||
Select an AI model provider to get started with goose.
|
|
||||||
</p>
|
|
||||||
<p className="text-sm text-textSubtle mb-8">
|
|
||||||
Click on a provider to configure its API keys and start using goose. Your keys are
|
|
||||||
stored securely and encrypted locally. You can change your provider and select
|
|
||||||
specific models in the settings.
|
|
||||||
</p>
|
|
||||||
<ProviderGrid onSubmit={onSubmit} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Get started (now less prominent) */}
|
|
||||||
<div className="mt-12">
|
|
||||||
<p className="text-sm text-textSubtle">
|
|
||||||
Not sure where to start?{' '}
|
|
||||||
<Button
|
|
||||||
variant="link"
|
|
||||||
className="text-blue-500 hover:text-blue-600 p-0 h-auto"
|
|
||||||
onClick={() =>
|
|
||||||
window.open('https://block.github.io/goose/docs/quickstart', '_blank')
|
|
||||||
}
|
|
||||||
>
|
|
||||||
Quick Start Guide
|
|
||||||
</Button>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</ScrollArea>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,13 +1,12 @@
|
|||||||
import { useState, useEffect, useRef } from 'react';
|
import { useState, useEffect, useRef } from 'react';
|
||||||
import { useModel } from '../settings/models/ModelContext';
|
|
||||||
import { AlertType, useAlerts } from '../alerts';
|
import { AlertType, useAlerts } from '../alerts';
|
||||||
import { useToolCount } from '../alerts/useToolCount';
|
import { useToolCount } from '../alerts/useToolCount';
|
||||||
import BottomMenuAlertPopover from './BottomMenuAlertPopover';
|
import BottomMenuAlertPopover from './BottomMenuAlertPopover';
|
||||||
import type { View, ViewOptions } from '../../App';
|
import type { View, ViewOptions } from '../../App';
|
||||||
import { BottomMenuModeSelection } from './BottomMenuModeSelection';
|
import { BottomMenuModeSelection } from './BottomMenuModeSelection';
|
||||||
import ModelsBottomBar from '../settings_v2/models/bottom_bar/ModelsBottomBar';
|
import ModelsBottomBar from '../settings/models/bottom_bar/ModelsBottomBar';
|
||||||
import { useConfig } from '../ConfigContext';
|
import { useConfig } from '../ConfigContext';
|
||||||
import { getCurrentModelAndProvider } from '../settings_v2/models';
|
import { useModelAndProvider } from '../ModelAndProviderContext';
|
||||||
import { Message } from '../../types/message';
|
import { Message } from '../../types/message';
|
||||||
import { ManualSummarizeButton } from '../context_management/ManualSummaryButton';
|
import { ManualSummarizeButton } from '../context_management/ManualSummaryButton';
|
||||||
|
|
||||||
@@ -34,11 +33,11 @@ export default function BottomMenu({
|
|||||||
setMessages: (messages: Message[]) => void;
|
setMessages: (messages: Message[]) => void;
|
||||||
}) {
|
}) {
|
||||||
const [isModelMenuOpen, setIsModelMenuOpen] = useState(false);
|
const [isModelMenuOpen, setIsModelMenuOpen] = useState(false);
|
||||||
const { currentModel } = useModel();
|
|
||||||
const { alerts, addAlert, clearAlerts } = useAlerts();
|
const { alerts, addAlert, clearAlerts } = useAlerts();
|
||||||
const dropdownRef = useRef<HTMLDivElement>(null);
|
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||||
const toolCount = useToolCount();
|
const toolCount = useToolCount();
|
||||||
const { getProviders, read } = useConfig();
|
const { getProviders, read } = useConfig();
|
||||||
|
const { getCurrentModelAndProvider, currentModel, currentProvider } = useModelAndProvider();
|
||||||
const [tokenLimit, setTokenLimit] = useState<number>(TOKEN_LIMIT_DEFAULT);
|
const [tokenLimit, setTokenLimit] = useState<number>(TOKEN_LIMIT_DEFAULT);
|
||||||
const [isTokenLimitLoaded, setIsTokenLimitLoaded] = useState(false);
|
const [isTokenLimitLoaded, setIsTokenLimitLoaded] = useState(false);
|
||||||
|
|
||||||
@@ -72,7 +71,7 @@ export default function BottomMenu({
|
|||||||
setIsTokenLimitLoaded(false);
|
setIsTokenLimitLoaded(false);
|
||||||
|
|
||||||
// Get current model and provider first to avoid unnecessary provider fetches
|
// Get current model and provider first to avoid unnecessary provider fetches
|
||||||
const { model, provider } = await getCurrentModelAndProvider({ readFromConfig: read });
|
const { model, provider } = await getCurrentModelAndProvider();
|
||||||
if (!model || !provider) {
|
if (!model || !provider) {
|
||||||
console.log('No model or provider found');
|
console.log('No model or provider found');
|
||||||
setIsTokenLimitLoaded(true);
|
setIsTokenLimitLoaded(true);
|
||||||
@@ -117,7 +116,7 @@ export default function BottomMenu({
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadProviderDetails();
|
loadProviderDetails();
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [currentModel]);
|
}, [currentModel, currentProvider]);
|
||||||
|
|
||||||
// Handle tool count alerts and token usage
|
// Handle tool count alerts and token usage
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useEffect, useRef, useState, useCallback } from 'react';
|
import { useEffect, useRef, useState, useCallback } from 'react';
|
||||||
import { all_goose_modes, ModeSelectionItem } from '../settings_v2/mode/ModeSelectionItem';
|
import { all_goose_modes, ModeSelectionItem } from '../settings/mode/ModeSelectionItem';
|
||||||
import { useConfig } from '../ConfigContext';
|
import { useConfig } from '../ConfigContext';
|
||||||
import { View, ViewOptions } from '../../App';
|
import { View, ViewOptions } from '../../App';
|
||||||
import { Orbit } from 'lucide-react';
|
import { Orbit } from 'lucide-react';
|
||||||
|
|||||||
@@ -1,472 +0,0 @@
|
|||||||
import { useState, useEffect, useRef } from 'react';
|
|
||||||
|
|
||||||
// Import actual PNG images
|
|
||||||
import llamaSprite from '../../assets/battle-game/llama.png';
|
|
||||||
import gooseSprite from '../../assets/battle-game/goose.png';
|
|
||||||
import battleBackground from '../../assets/battle-game/background.png';
|
|
||||||
import battleMusic from '../../assets/battle-game/battle.mp3';
|
|
||||||
|
|
||||||
interface BattleState {
|
|
||||||
currentStep: number;
|
|
||||||
gooseHp: number;
|
|
||||||
llamaHp: number;
|
|
||||||
message: string;
|
|
||||||
animation: string | null;
|
|
||||||
lastChoice?: string;
|
|
||||||
showHostInput?: boolean;
|
|
||||||
processingAction?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface OllamaBattleGameProps {
|
|
||||||
onComplete: (configValues: { [key: string]: string }) => void;
|
|
||||||
requiredKeys: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export function OllamaBattleGame({ onComplete, requiredKeys: _ }: OllamaBattleGameProps) {
|
|
||||||
// Use Audio element type for audioRef
|
|
||||||
// eslint-disable-next-line no-undef
|
|
||||||
const audioRef = useRef<HTMLAudioElement | null>(null);
|
|
||||||
const [isMuted, setIsMuted] = useState(false);
|
|
||||||
|
|
||||||
const [battleState, setBattleState] = useState<BattleState>({
|
|
||||||
currentStep: 0,
|
|
||||||
gooseHp: 100,
|
|
||||||
llamaHp: 100,
|
|
||||||
message: 'A wild Ollama appeared!',
|
|
||||||
animation: null,
|
|
||||||
processingAction: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
const [configValues, setConfigValues] = useState<{ [key: string]: string }>({});
|
|
||||||
|
|
||||||
// Initialize audio when component mounts
|
|
||||||
useEffect(() => {
|
|
||||||
if (typeof window !== 'undefined') {
|
|
||||||
audioRef.current = new window.Audio(battleMusic);
|
|
||||||
audioRef.current.loop = true;
|
|
||||||
audioRef.current.volume = 0.2;
|
|
||||||
audioRef.current.play().catch((e) => console.log('Audio autoplay prevented:', e));
|
|
||||||
}
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
if (audioRef.current) {
|
|
||||||
audioRef.current.pause();
|
|
||||||
audioRef.current = null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const toggleMute = () => {
|
|
||||||
if (audioRef.current) {
|
|
||||||
if (isMuted) {
|
|
||||||
audioRef.current.volume = 0.2;
|
|
||||||
} else {
|
|
||||||
audioRef.current.volume = 0;
|
|
||||||
}
|
|
||||||
setIsMuted(!isMuted);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const battleSteps = [
|
|
||||||
{
|
|
||||||
message: 'A wild Ollama appeared!',
|
|
||||||
action: null,
|
|
||||||
animation: 'appear',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
message: 'What will GOOSE do?',
|
|
||||||
action: 'choice',
|
|
||||||
choices: ['Pacify', 'HONK!'],
|
|
||||||
animation: 'attack',
|
|
||||||
followUpMessages: ["It's not very effective...", 'But OLLAMA is confused!'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
message: 'OLLAMA used YAML Confusion!',
|
|
||||||
action: null,
|
|
||||||
animation: 'counter',
|
|
||||||
followUpMessages: ['OLLAMA hurt itself in confusion!', 'GOOSE maintained composure!'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
message: 'What will GOOSE do?',
|
|
||||||
action: 'final_choice',
|
|
||||||
choices: (previousChoice: string) => [
|
|
||||||
previousChoice === 'Pacify' ? 'HONK!' : 'Pacify',
|
|
||||||
'Configure Host',
|
|
||||||
],
|
|
||||||
animation: 'attack',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
message: 'OLLAMA used Docker Dependency!',
|
|
||||||
action: null,
|
|
||||||
animation: 'counter',
|
|
||||||
followUpMessages: ["It's not very effective...", 'GOOSE knows containerization!'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
message: 'What will GOOSE do?',
|
|
||||||
action: 'host_choice',
|
|
||||||
choices: ['Configure Host'],
|
|
||||||
animation: 'finish',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
message: '', // Will be set dynamically based on choice
|
|
||||||
action: 'host_input',
|
|
||||||
prompt: 'Enter your Ollama host address:',
|
|
||||||
configKey: 'OLLAMA_HOST',
|
|
||||||
animation: 'finish',
|
|
||||||
followUpMessages: [
|
|
||||||
"It's super effective!",
|
|
||||||
'OLLAMA has been configured!',
|
|
||||||
'OLLAMA joined your team!',
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
message: 'Configuration complete!\nOLLAMA will remember this friendship!',
|
|
||||||
action: 'complete',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const animateHit = (isLlama: boolean) => {
|
|
||||||
const element = document.querySelector(isLlama ? '.llama-sprite' : '.goose-sprite');
|
|
||||||
if (element) {
|
|
||||||
element.classList.add('hit-flash');
|
|
||||||
setTimeout(() => {
|
|
||||||
element.classList.remove('hit-flash');
|
|
||||||
}, 500);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
// Add CSS for the hit animation and defeat animation
|
|
||||||
const style = document.createElement('style');
|
|
||||||
style.textContent = `
|
|
||||||
@keyframes hitFlash {
|
|
||||||
0%, 100% { opacity: 1; }
|
|
||||||
50% { opacity: 0; }
|
|
||||||
}
|
|
||||||
.hit-flash {
|
|
||||||
animation: hitFlash 0.5s;
|
|
||||||
}
|
|
||||||
@keyframes defeat {
|
|
||||||
0% { transform: translateY(0); opacity: 1; }
|
|
||||||
20% { transform: translateY(-30px); opacity: 1; }
|
|
||||||
100% { transform: translateY(500px); opacity: 0; }
|
|
||||||
}
|
|
||||||
.defeated {
|
|
||||||
animation: defeat 1.3s cubic-bezier(.36,.07,.19,.97) both;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
document.head.appendChild(style);
|
|
||||||
return () => {
|
|
||||||
document.head.removeChild(style);
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleAction = async (value: string) => {
|
|
||||||
const currentStep =
|
|
||||||
battleState.currentStep < battleSteps.length ? battleSteps[battleState.currentStep] : null;
|
|
||||||
|
|
||||||
if (!currentStep) return;
|
|
||||||
|
|
||||||
// Handle host input
|
|
||||||
if (currentStep.action === 'host_input' && value && currentStep.configKey) {
|
|
||||||
setConfigValues((prev) => ({
|
|
||||||
...prev,
|
|
||||||
[currentStep.configKey!]: value,
|
|
||||||
}));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle host submit
|
|
||||||
if (currentStep.action === 'host_input' && !value) {
|
|
||||||
setBattleState((prev) => ({
|
|
||||||
...prev,
|
|
||||||
processingAction: true,
|
|
||||||
llamaHp: 0,
|
|
||||||
message: "It's super effective!",
|
|
||||||
}));
|
|
||||||
animateHit(true);
|
|
||||||
|
|
||||||
// Add defeat class to llama sprite and health bar
|
|
||||||
const llamaContainer = document.querySelector('.llama-container');
|
|
||||||
if (llamaContainer) {
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
||||||
llamaContainer.classList.add('defeated');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Show victory messages with delays
|
|
||||||
if (currentStep.followUpMessages) {
|
|
||||||
for (const msg of currentStep.followUpMessages) {
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
||||||
setBattleState((prev) => ({ ...prev, message: msg }));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
||||||
onComplete(configValues);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle continue button for messages
|
|
||||||
if (!currentStep.action) {
|
|
||||||
setBattleState((prev) => ({
|
|
||||||
...prev,
|
|
||||||
currentStep: prev.currentStep + 1,
|
|
||||||
message: battleSteps[prev.currentStep + 1]?.message || prev.message,
|
|
||||||
processingAction: false,
|
|
||||||
}));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle choices (Pacify/HONK/Configure Host)
|
|
||||||
if (
|
|
||||||
(currentStep.action === 'choice' ||
|
|
||||||
currentStep.action === 'final_choice' ||
|
|
||||||
currentStep.action === 'host_choice') &&
|
|
||||||
value
|
|
||||||
) {
|
|
||||||
// Set processing flag to hide buttons
|
|
||||||
setBattleState((prev) => ({
|
|
||||||
...prev,
|
|
||||||
processingAction: true,
|
|
||||||
}));
|
|
||||||
|
|
||||||
if (value === 'Configure Host') {
|
|
||||||
setBattleState((prev) => ({
|
|
||||||
...prev,
|
|
||||||
message: 'GOOSE used Configure Host!',
|
|
||||||
showHostInput: true,
|
|
||||||
currentStep: battleSteps.findIndex((step) => step.action === 'host_input'),
|
|
||||||
processingAction: false,
|
|
||||||
}));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle Pacify or HONK attacks
|
|
||||||
setBattleState((prev) => ({
|
|
||||||
...prev,
|
|
||||||
lastChoice: value,
|
|
||||||
llamaHp: Math.max(0, prev.llamaHp - 25),
|
|
||||||
message: `GOOSE used ${value}!`,
|
|
||||||
}));
|
|
||||||
animateHit(true);
|
|
||||||
|
|
||||||
// Show follow-up messages
|
|
||||||
if (currentStep.followUpMessages) {
|
|
||||||
for (const msg of currentStep.followUpMessages) {
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
||||||
setBattleState((prev) => ({ ...prev, message: msg }));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Proceed to counter-attack
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
||||||
const isFirstCycle = currentStep.action === 'choice';
|
|
||||||
const nextStep = battleSteps[battleState.currentStep + 1];
|
|
||||||
setBattleState((prev) => ({
|
|
||||||
...prev,
|
|
||||||
gooseHp: Math.max(0, prev.gooseHp - 25),
|
|
||||||
message: isFirstCycle ? 'OLLAMA used YAML Confusion!' : 'OLLAMA used Docker Dependency!',
|
|
||||||
currentStep: prev.currentStep + 1,
|
|
||||||
processingAction: false,
|
|
||||||
}));
|
|
||||||
animateHit(false);
|
|
||||||
|
|
||||||
// Show counter-attack messages
|
|
||||||
if (nextStep?.followUpMessages) {
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
||||||
for (const msg of nextStep.followUpMessages) {
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
||||||
setBattleState((prev) => ({ ...prev, message: msg }));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for battle completion
|
|
||||||
if (battleState.currentStep === battleSteps.length - 2) {
|
|
||||||
onComplete(configValues);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="w-full h-full px-4 py-6">
|
|
||||||
{/* Battle Scene */}
|
|
||||||
<div
|
|
||||||
className="relative w-full h-[300px] rounded-lg mb-4 bg-cover bg-center border-4 border-[#2C3E50] overflow-hidden"
|
|
||||||
style={{
|
|
||||||
backgroundImage: `url(${battleBackground})`,
|
|
||||||
backgroundSize: 'cover',
|
|
||||||
backgroundPosition: 'center bottom',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{/* Llama sprite */}
|
|
||||||
<div className="absolute right-24 top-8 llama-container">
|
|
||||||
<div className="mb-2">
|
|
||||||
<div className="bg-[#1F2937] rounded-lg px-3 py-1 text-white font-pokemon mb-1">
|
|
||||||
OLLAMA
|
|
||||||
<span className="text-xs ml-2">Lv.1</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<div className="h-2 bg-[#374151] rounded-full flex-grow">
|
|
||||||
<div
|
|
||||||
className="h-full rounded-full transition-all duration-300"
|
|
||||||
style={{
|
|
||||||
width: `${battleState.llamaHp}%`,
|
|
||||||
backgroundColor:
|
|
||||||
battleState.llamaHp > 50
|
|
||||||
? '#10B981'
|
|
||||||
: battleState.llamaHp > 20
|
|
||||||
? '#F59E0B'
|
|
||||||
: '#EF4444',
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<span className="text-sm font-pokemon text-[#1F2937]">
|
|
||||||
{Math.floor(battleState.llamaHp)}/100
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<img
|
|
||||||
src={llamaSprite}
|
|
||||||
alt="Llama"
|
|
||||||
className="w-40 h-40 object-contain llama-sprite pixelated"
|
|
||||||
style={{
|
|
||||||
transform: `translateY(${battleState.currentStep % 2 === 1 ? '-4px' : '0'})`,
|
|
||||||
transition: 'transform 0.3s ease-in-out',
|
|
||||||
imageRendering: 'pixelated',
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Goose sprite */}
|
|
||||||
<div className="absolute left-24 bottom-4">
|
|
||||||
<img
|
|
||||||
src={gooseSprite}
|
|
||||||
alt="Goose"
|
|
||||||
className="w-40 h-40 object-contain mb-2 goose-sprite pixelated"
|
|
||||||
style={{
|
|
||||||
transform: `translateY(${battleState.currentStep % 2 === 0 ? '-4px' : '0'})`,
|
|
||||||
transition: 'transform 0.3s ease-in-out',
|
|
||||||
imageRendering: 'pixelated',
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<div>
|
|
||||||
<div className="bg-[#1F2937] rounded-lg px-3 py-1 text-white font-pokemon mb-1">
|
|
||||||
GOOSE
|
|
||||||
<span className="text-xs ml-2">Lv.99</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<div className="h-2 bg-[#374151] rounded-full flex-grow">
|
|
||||||
<div
|
|
||||||
className="h-full rounded-full transition-all duration-300"
|
|
||||||
style={{
|
|
||||||
width: `${battleState.gooseHp}%`,
|
|
||||||
backgroundColor:
|
|
||||||
battleState.gooseHp > 50
|
|
||||||
? '#10B981'
|
|
||||||
: battleState.gooseHp > 20
|
|
||||||
? '#F59E0B'
|
|
||||||
: '#EF4444',
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<span className="text-sm font-pokemon text-[#1F2937]">
|
|
||||||
{Math.floor(battleState.gooseHp)}/100
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Dialog Box */}
|
|
||||||
<div className="relative w-full">
|
|
||||||
<div className="w-full bg-[#1F2937] rounded-lg p-6 border-4 border-[#4B5563] shadow-lg">
|
|
||||||
<div className="absolute top-4 right-4">
|
|
||||||
<button
|
|
||||||
onClick={toggleMute}
|
|
||||||
className="text-white hover:text-gray-300 transition-colors"
|
|
||||||
>
|
|
||||||
{isMuted ? '🔇' : '🔊'}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<p className="text-lg mb-4 text-white font-pokemon leading-relaxed">
|
|
||||||
{battleState.message}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
{battleState.currentStep < battleSteps.length && (
|
|
||||||
<div className="space-y-4">
|
|
||||||
{/* Show battle choices */}
|
|
||||||
{(battleSteps[battleState.currentStep].action === 'choice' ||
|
|
||||||
battleSteps[battleState.currentStep].action === 'final_choice' ||
|
|
||||||
battleSteps[battleState.currentStep].action === 'host_choice') &&
|
|
||||||
!battleState.showHostInput &&
|
|
||||||
!battleState.processingAction && (
|
|
||||||
<div className="space-y-2">
|
|
||||||
{(typeof battleSteps[battleState.currentStep].choices === 'function'
|
|
||||||
? (
|
|
||||||
battleSteps[battleState.currentStep].choices as (
|
|
||||||
choice: string
|
|
||||||
) => string[]
|
|
||||||
)(battleState.lastChoice || '')
|
|
||||||
: (battleSteps[battleState.currentStep].choices as string[])
|
|
||||||
)?.map((choice: string) => (
|
|
||||||
<button
|
|
||||||
key={choice}
|
|
||||||
onClick={() => handleAction(choice)}
|
|
||||||
className="w-full text-left px-4 py-2 text-white font-pokemon hover:bg-[#2563EB] transition-colors rounded-lg group flex items-center"
|
|
||||||
>
|
|
||||||
<span className="opacity-0 group-hover:opacity-100 transition-opacity mr-2">
|
|
||||||
▶
|
|
||||||
</span>
|
|
||||||
{choice}
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Show host input when needed */}
|
|
||||||
{battleState.showHostInput && !battleState.processingAction && (
|
|
||||||
<div className="space-y-4">
|
|
||||||
<p className="text-sm text-gray-300 font-pokemon">
|
|
||||||
Enter your Ollama host address:
|
|
||||||
</p>
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
className="flex-grow px-4 py-2 bg-[#374151] border-2 border-[#4B5563] rounded-lg text-white font-pokemon placeholder-gray-400 focus:outline-none focus:border-[#60A5FA]"
|
|
||||||
placeholder="http://localhost:11434"
|
|
||||||
onChange={(e) => handleAction(e.target.value)}
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
onClick={() => handleAction('')}
|
|
||||||
className="px-6 py-2 bg-[#2563EB] text-white font-pokemon rounded-lg hover:bg-[#1D4ED8] transition-colors focus:outline-none focus:ring-2 focus:ring-[#60A5FA] focus:ring-opacity-50"
|
|
||||||
>
|
|
||||||
Submit
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Continue button for messages */}
|
|
||||||
{!battleSteps[battleState.currentStep].action && !battleState.processingAction && (
|
|
||||||
<button
|
|
||||||
onClick={() => handleAction('')}
|
|
||||||
className="mt-2 px-6 py-2 bg-[#2563EB] text-white font-pokemon rounded-lg hover:bg-[#1D4ED8] transition-colors focus:outline-none focus:ring-2 focus:ring-[#60A5FA] focus:ring-opacity-50"
|
|
||||||
>
|
|
||||||
▶ Continue
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Black corners for that classic Pokemon feel */}
|
|
||||||
<div className="absolute top-0 left-0 w-4 h-4 bg-black rounded-tl-lg"></div>
|
|
||||||
<div className="absolute top-0 right-0 w-4 h-4 bg-black rounded-tr-lg"></div>
|
|
||||||
<div className="absolute bottom-0 left-0 w-4 h-4 bg-black rounded-bl-lg"></div>
|
|
||||||
<div className="absolute bottom-0 right-0 w-4 h-4 bg-black rounded-br-lg"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,117 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { Card } from '../ui/card';
|
|
||||||
import { Lock } from 'lucide-react';
|
|
||||||
import { Input } from '../ui/input';
|
|
||||||
import { Button } from '../ui/button';
|
|
||||||
import { required_keys, default_key_value } from './models/hardcoded_stuff';
|
|
||||||
import { isSecretKey } from './api_keys/utils';
|
|
||||||
import { OllamaBattleGame } from './OllamaBattleGame';
|
|
||||||
|
|
||||||
interface ProviderSetupModalProps {
|
|
||||||
provider: string;
|
|
||||||
_model: string;
|
|
||||||
_endpoint: string;
|
|
||||||
title?: string;
|
|
||||||
onSubmit: (configValues: { [key: string]: string }) => void;
|
|
||||||
onCancel: () => void;
|
|
||||||
forceBattle?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ProviderSetupModal({
|
|
||||||
provider,
|
|
||||||
_model: _,
|
|
||||||
_endpoint: __,
|
|
||||||
title,
|
|
||||||
onSubmit,
|
|
||||||
onCancel,
|
|
||||||
forceBattle = false,
|
|
||||||
}: ProviderSetupModalProps) {
|
|
||||||
const [configValues, setConfigValues] = React.useState<{ [key: string]: string }>(
|
|
||||||
default_key_value
|
|
||||||
);
|
|
||||||
const requiredKeys = (required_keys as Record<string, string[]>)[provider] || ['API Key'];
|
|
||||||
const headerText = title || `Setup ${provider}`;
|
|
||||||
|
|
||||||
const shouldShowBattle = React.useMemo(() => {
|
|
||||||
if (forceBattle) return true;
|
|
||||||
if (provider.toLowerCase() !== 'ollama') return false;
|
|
||||||
|
|
||||||
const now = new Date();
|
|
||||||
return now.getMinutes() === 0;
|
|
||||||
}, [provider, forceBattle]);
|
|
||||||
|
|
||||||
const handleSubmit = (e: React.FormEvent) => {
|
|
||||||
e.preventDefault();
|
|
||||||
onSubmit(configValues);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="fixed inset-0 bg-black/20 dark:bg-white/20 backdrop-blur-sm transition-colors animate-[fadein_200ms_ease-in_forwards]">
|
|
||||||
<Card className="fixed top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[500px] bg-bgApp rounded-xl overflow-hidden shadow-none p-[16px] pt-[24px] pb-0">
|
|
||||||
<div className="px-4 pb-0 space-y-8">
|
|
||||||
{/* Header */}
|
|
||||||
<div className="flex">
|
|
||||||
<h2 className="text-2xl font-regular text-textStandard">{headerText}</h2>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{provider.toLowerCase() === 'ollama' && shouldShowBattle ? (
|
|
||||||
<OllamaBattleGame onComplete={onSubmit} requiredKeys={requiredKeys} />
|
|
||||||
) : (
|
|
||||||
<form onSubmit={handleSubmit}>
|
|
||||||
<div className="mt-[24px] space-y-4">
|
|
||||||
{requiredKeys.map((keyName: string) => (
|
|
||||||
<div key={keyName}>
|
|
||||||
<Input
|
|
||||||
type={isSecretKey(keyName) ? 'password' : 'text'}
|
|
||||||
value={configValues[keyName] || ''}
|
|
||||||
onChange={(e) =>
|
|
||||||
setConfigValues((prev) => ({
|
|
||||||
...prev,
|
|
||||||
[keyName]: e.target.value,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
placeholder={keyName}
|
|
||||||
className="w-full h-14 px-4 font-regular rounded-lg border shadow-none border-gray-300 bg-white text-lg placeholder:text-gray-400 font-regular text-gray-900"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
<div
|
|
||||||
className="flex text-gray-600 dark:text-gray-300"
|
|
||||||
onClick={() => {
|
|
||||||
if (provider.toLowerCase() === 'ollama') {
|
|
||||||
onCancel();
|
|
||||||
onSubmit({ forceBattle: 'true' });
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Lock className="w-6 h-6" />
|
|
||||||
<span className="text-sm font-light ml-4 mt-[2px]">{`Your configuration values will be stored securely in the keychain and used only for making requests to ${provider}`}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Actions */}
|
|
||||||
<div className="mt-[8px] -ml-8 -mr-8 pt-8">
|
|
||||||
<Button
|
|
||||||
type="submit"
|
|
||||||
variant="ghost"
|
|
||||||
className="w-full h-[60px] rounded-none border-t border-borderSubtle text-md hover:bg-bgSubtle text-textProminent font-regular"
|
|
||||||
>
|
|
||||||
Submit
|
|
||||||
</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-md font-regular"
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,57 +1,19 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
|
||||||
import { IpcRendererEvent } from 'electron';
|
|
||||||
import { ScrollArea } from '../ui/scroll-area';
|
import { ScrollArea } from '../ui/scroll-area';
|
||||||
import { Settings as SettingsType } from './types';
|
|
||||||
import {
|
|
||||||
FullExtensionConfig,
|
|
||||||
addExtension,
|
|
||||||
removeExtension,
|
|
||||||
BUILT_IN_EXTENSIONS,
|
|
||||||
} from '../../extensions';
|
|
||||||
import { ConfigureExtensionModal } from './extensions/ConfigureExtensionModal';
|
|
||||||
import { ManualExtensionModal } from './extensions/ManualExtensionModal';
|
|
||||||
import { ConfigureBuiltInExtensionModal } from './extensions/ConfigureBuiltInExtensionModal';
|
|
||||||
import BackButton from '../ui/BackButton';
|
import BackButton from '../ui/BackButton';
|
||||||
import { RecentModelsRadio } from './models/RecentModels';
|
import type { View, ViewOptions } from '../../App';
|
||||||
import { ExtensionItem } from './extensions/ExtensionItem';
|
import ExtensionsSection from './extensions/ExtensionsSection';
|
||||||
import { View, ViewOptions } from '../../App';
|
import ModelsSection from './models/ModelsSection';
|
||||||
import { ModeSelection } from './basic/ModeSelection';
|
import { ModeSection } from './mode/ModeSection';
|
||||||
import SessionSharingSection from './session/SessionSharingSection';
|
import { ToolSelectionStrategySection } from './tool_selection_strategy/ToolSelectionStrategySection';
|
||||||
import { toastSuccess } from '../../toasts';
|
import SessionSharingSection from './sessions/SessionSharingSection';
|
||||||
|
import { ResponseStylesSection } from './response_styles/ResponseStylesSection';
|
||||||
|
import AppSettingsSection from './app/AppSettingsSection';
|
||||||
|
import { ExtensionConfig } from '../../api';
|
||||||
import MoreMenuLayout from '../more_menu/MoreMenuLayout';
|
import MoreMenuLayout from '../more_menu/MoreMenuLayout';
|
||||||
|
|
||||||
const EXTENSIONS_DESCRIPTION =
|
|
||||||
'The Model Context Protocol (MCP) is a system that allows AI models to securely connect with local or remote resources using standard server setups. It works like a client-server setup and expands AI capabilities using three main components: Prompts, Resources, and Tools.';
|
|
||||||
|
|
||||||
const EXTENSIONS_SITE_LINK = 'https://block.github.io/goose/v1/extensions/';
|
|
||||||
|
|
||||||
const DEFAULT_SETTINGS: SettingsType = {
|
|
||||||
models: [
|
|
||||||
{
|
|
||||||
id: 'gpt4',
|
|
||||||
name: 'GPT 4.0',
|
|
||||||
description: 'Standard config',
|
|
||||||
enabled: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'gpt4lite',
|
|
||||||
name: 'GPT 4.0 lite',
|
|
||||||
description: 'Standard config',
|
|
||||||
enabled: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'claude',
|
|
||||||
name: 'Claude',
|
|
||||||
description: 'Standard config',
|
|
||||||
enabled: true,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
extensions: BUILT_IN_EXTENSIONS,
|
|
||||||
};
|
|
||||||
|
|
||||||
export type SettingsViewOptions = {
|
export type SettingsViewOptions = {
|
||||||
extensionId: string;
|
deepLinkConfig?: ExtensionConfig;
|
||||||
showEnvVars: boolean;
|
showEnvVars?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function SettingsView({
|
export default function SettingsView({
|
||||||
@@ -63,133 +25,8 @@ export default function SettingsView({
|
|||||||
setView: (view: View, viewOptions?: ViewOptions) => void;
|
setView: (view: View, viewOptions?: ViewOptions) => void;
|
||||||
viewOptions: SettingsViewOptions;
|
viewOptions: SettingsViewOptions;
|
||||||
}) {
|
}) {
|
||||||
const [settings, setSettings] = React.useState<SettingsType>(() => {
|
|
||||||
const saved = localStorage.getItem('user_settings');
|
|
||||||
window.electron.logInfo('Settings: ' + saved);
|
|
||||||
let currentSettings = saved ? JSON.parse(saved) : DEFAULT_SETTINGS;
|
|
||||||
|
|
||||||
// Ensure built-in extensions are included if not already present
|
|
||||||
BUILT_IN_EXTENSIONS.forEach((builtIn) => {
|
|
||||||
const exists = currentSettings.extensions.some(
|
|
||||||
(ext: FullExtensionConfig) => ext.id === builtIn.id
|
|
||||||
);
|
|
||||||
if (!exists) {
|
|
||||||
currentSettings.extensions.push(builtIn);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return currentSettings;
|
|
||||||
});
|
|
||||||
|
|
||||||
const [extensionBeingConfigured, setExtensionBeingConfigured] =
|
|
||||||
useState<FullExtensionConfig | null>(null);
|
|
||||||
|
|
||||||
const [isManualModalOpen, setIsManualModalOpen] = useState(false);
|
|
||||||
|
|
||||||
// Persist settings changes
|
|
||||||
useEffect(() => {
|
|
||||||
localStorage.setItem('user_settings', JSON.stringify(settings));
|
|
||||||
}, [settings]);
|
|
||||||
|
|
||||||
// Listen for settings updates from extension storage
|
|
||||||
useEffect(() => {
|
|
||||||
const handleSettingsUpdate = (_event: IpcRendererEvent) => {
|
|
||||||
const saved = localStorage.getItem('user_settings');
|
|
||||||
if (saved) {
|
|
||||||
let currentSettings = JSON.parse(saved);
|
|
||||||
setSettings(currentSettings);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
window.electron.on('settings-updated', handleSettingsUpdate);
|
|
||||||
return () => {
|
|
||||||
window.electron.off('settings-updated', handleSettingsUpdate);
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// Handle URL parameters for auto-opening extension configuration
|
|
||||||
useEffect(() => {
|
|
||||||
const extensionId = viewOptions.extensionId;
|
|
||||||
const showEnvVars = viewOptions.showEnvVars;
|
|
||||||
|
|
||||||
if (extensionId && showEnvVars === true) {
|
|
||||||
// Find the extension in settings
|
|
||||||
const extension = settings.extensions.find((ext) => ext.id === extensionId);
|
|
||||||
if (extension) {
|
|
||||||
// Auto-open the configuration modal
|
|
||||||
setExtensionBeingConfigured(extension);
|
|
||||||
// Scroll to extensions section
|
|
||||||
const element = document.getElementById('extensions');
|
|
||||||
if (element) {
|
|
||||||
element.scrollIntoView({ behavior: 'smooth' });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// We only run this once on load
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [settings.extensions]);
|
|
||||||
|
|
||||||
const handleExtensionToggle = async (extensionId: string) => {
|
|
||||||
// Find the extension to get its current state
|
|
||||||
const extension = settings.extensions.find((ext) => ext.id === extensionId);
|
|
||||||
|
|
||||||
if (!extension) return;
|
|
||||||
|
|
||||||
const newEnabled = !extension.enabled;
|
|
||||||
|
|
||||||
const originalSettings = settings;
|
|
||||||
|
|
||||||
// Optimistically update local component state
|
|
||||||
setSettings((prev) => ({
|
|
||||||
...prev,
|
|
||||||
extensions: prev.extensions.map((ext) =>
|
|
||||||
ext.id === extensionId ? { ...ext, enabled: newEnabled } : ext
|
|
||||||
),
|
|
||||||
}));
|
|
||||||
|
|
||||||
let response: Response;
|
|
||||||
|
|
||||||
if (newEnabled) {
|
|
||||||
response = await addExtension(extension);
|
|
||||||
} else {
|
|
||||||
response = await removeExtension(extension.name);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
setSettings(originalSettings);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleExtensionRemove = async () => {
|
|
||||||
if (!extensionBeingConfigured) return;
|
|
||||||
|
|
||||||
const response = await removeExtension(extensionBeingConfigured.name, true);
|
|
||||||
|
|
||||||
if (response.ok) {
|
|
||||||
toastSuccess({
|
|
||||||
title: extensionBeingConfigured.name,
|
|
||||||
msg: `Successfully removed extension`,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Remove from localstorage
|
|
||||||
setSettings((prev) => ({
|
|
||||||
...prev,
|
|
||||||
extensions: prev.extensions.filter((ext) => ext.id !== extensionBeingConfigured.id),
|
|
||||||
}));
|
|
||||||
setExtensionBeingConfigured(null);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleExtensionConfigSubmit = () => {
|
|
||||||
setExtensionBeingConfigured(null);
|
|
||||||
};
|
|
||||||
|
|
||||||
const isBuiltIn = (extensionId: string) => {
|
|
||||||
return BUILT_IN_EXTENSIONS.some((builtIn) => builtIn.id === extensionId);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-screen w-full">
|
<div className="h-screen w-full animate-[fadein_200ms_ease-in_forwards]">
|
||||||
<MoreMenuLayout showMenu={false} />
|
<MoreMenuLayout showMenu={false} />
|
||||||
|
|
||||||
<ScrollArea className="h-full w-full">
|
<ScrollArea className="h-full w-full">
|
||||||
@@ -200,114 +37,29 @@ export default function SettingsView({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Content Area */}
|
{/* Content Area */}
|
||||||
<div className="flex-1 py-8 pt-[20px]">
|
<div className="flex-1 pt-[20px]">
|
||||||
<div className="space-y-8">
|
<div className="space-y-8">
|
||||||
{/*Models Section*/}
|
{/* Models Section */}
|
||||||
<section id="models">
|
<ModelsSection setView={setView} />
|
||||||
<RecentModelsRadio setView={setView} />
|
{/* Extensions Section */}
|
||||||
</section>
|
<ExtensionsSection
|
||||||
<section id="extensions">
|
deepLinkConfig={viewOptions.deepLinkConfig}
|
||||||
<div className="flex justify-between items-center mb-6 border-b border-borderSubtle px-8">
|
showEnvVars={viewOptions.showEnvVars}
|
||||||
<h2 className="text-xl font-semibold text-textStandard">Extensions</h2>
|
/>
|
||||||
<a
|
{/* Goose Modes */}
|
||||||
href={EXTENSIONS_SITE_LINK}
|
<ModeSection setView={setView} />
|
||||||
target="_blank"
|
{/*Session sharing*/}
|
||||||
className="text-indigo-500 hover:text-indigo-600 text-sm"
|
<SessionSharingSection />
|
||||||
rel="noreferrer"
|
{/* Response Styles */}
|
||||||
>
|
<ResponseStylesSection />
|
||||||
Browse
|
{/* Tool Selection Strategy */}
|
||||||
</a>
|
<ToolSelectionStrategySection setView={setView} />
|
||||||
</div>
|
{/* App Settings */}
|
||||||
|
<AppSettingsSection />
|
||||||
<div className="px-8">
|
|
||||||
<p className="text-sm text-textStandard mb-4">{EXTENSIONS_DESCRIPTION}</p>
|
|
||||||
|
|
||||||
{settings.extensions.length === 0 ? (
|
|
||||||
<p className="text-textSubtle text-center py-4">No Extensions Added</p>
|
|
||||||
) : (
|
|
||||||
<div className="grid grid-cols-2 gap-2">
|
|
||||||
{settings.extensions.map((ext) => (
|
|
||||||
<ExtensionItem
|
|
||||||
key={ext.id}
|
|
||||||
{...ext}
|
|
||||||
canConfigure={true}
|
|
||||||
onToggle={handleExtensionToggle}
|
|
||||||
onConfigure={(extension) => setExtensionBeingConfigured(extension)}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
<button
|
|
||||||
onClick={() => setIsManualModalOpen(true)}
|
|
||||||
className="text-indigo-500 hover:text-indigo-600 text-sm"
|
|
||||||
title="Add Manually"
|
|
||||||
>
|
|
||||||
<div className="rounded-lg border border-dashed border-borderSubtle hover:border-borderStandard p-4 transition-colors">
|
|
||||||
Add custom extension
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section id="mode">
|
|
||||||
<div className="flex justify-between items-center mb-6 border-b border-borderSubtle px-8">
|
|
||||||
<h2 className="text-xl font-semibold text-textStandard">Mode Selection</h2>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="px-8">
|
|
||||||
<p className="text-sm text-textStandard mb-4">
|
|
||||||
Configure how Goose interacts with tools and extensions
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<ModeSelection />
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
<section id="session-sharing">
|
|
||||||
<SessionSharingSection />
|
|
||||||
</section>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
|
|
||||||
{extensionBeingConfigured && isBuiltIn(extensionBeingConfigured.id) ? (
|
|
||||||
<ConfigureBuiltInExtensionModal
|
|
||||||
isOpen={!!extensionBeingConfigured && isBuiltIn(extensionBeingConfigured.id)}
|
|
||||||
onClose={() => {
|
|
||||||
setExtensionBeingConfigured(null);
|
|
||||||
}}
|
|
||||||
extension={extensionBeingConfigured}
|
|
||||||
onSubmit={handleExtensionConfigSubmit}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<ConfigureExtensionModal
|
|
||||||
isOpen={!!extensionBeingConfigured}
|
|
||||||
onClose={() => {
|
|
||||||
setExtensionBeingConfigured(null);
|
|
||||||
}}
|
|
||||||
extension={extensionBeingConfigured}
|
|
||||||
onSubmit={handleExtensionConfigSubmit}
|
|
||||||
onRemove={handleExtensionRemove}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<ManualExtensionModal
|
|
||||||
isOpen={isManualModalOpen}
|
|
||||||
onClose={() => setIsManualModalOpen(false)}
|
|
||||||
onSubmit={async (extension) => {
|
|
||||||
const response = await addExtension(extension);
|
|
||||||
|
|
||||||
if (response.ok) {
|
|
||||||
setSettings((prev) => ({
|
|
||||||
...prev,
|
|
||||||
extensions: [...prev.extensions, extension],
|
|
||||||
}));
|
|
||||||
setIsManualModalOpen(false);
|
|
||||||
} else {
|
|
||||||
// TODO - Anything for the UI state beyond validation?
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,49 +0,0 @@
|
|||||||
import { createContext, useContext, useState, ReactNode, useEffect } from 'react';
|
|
||||||
import { getActiveProviders } from './utils';
|
|
||||||
import SuspenseLoader from '../../../suspense-loader';
|
|
||||||
|
|
||||||
// Create a context for active keys
|
|
||||||
const ActiveKeysContext = createContext<
|
|
||||||
| {
|
|
||||||
activeKeys: string[];
|
|
||||||
setActiveKeys: (keys: string[]) => void;
|
|
||||||
}
|
|
||||||
| undefined
|
|
||||||
>(undefined);
|
|
||||||
|
|
||||||
export const ActiveKeysProvider = ({ children }: { children: ReactNode }) => {
|
|
||||||
const [activeKeys, setActiveKeys] = useState<string[]>([]); // Start with an empty list
|
|
||||||
const [isLoading, setIsLoading] = useState(true); // Track loading state
|
|
||||||
|
|
||||||
// Fetch active keys from the backend
|
|
||||||
useEffect(() => {
|
|
||||||
const fetchActiveProviders = async () => {
|
|
||||||
try {
|
|
||||||
const providers = await getActiveProviders(); // Fetch the active providers
|
|
||||||
setActiveKeys(providers); // Update state with fetched providers
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error fetching active providers:', error);
|
|
||||||
} finally {
|
|
||||||
setIsLoading(false); // Ensure loading is marked as complete
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
fetchActiveProviders(); // Call the async function
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// Provide active keys and ability to update them
|
|
||||||
return (
|
|
||||||
<ActiveKeysContext.Provider value={{ activeKeys, setActiveKeys }}>
|
|
||||||
{!isLoading ? children : <SuspenseLoader />}
|
|
||||||
</ActiveKeysContext.Provider>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Custom hook to access active keys
|
|
||||||
export const useActiveKeys = () => {
|
|
||||||
const context = useContext(ActiveKeysContext);
|
|
||||||
if (!context) {
|
|
||||||
throw new Error('useActiveKeys must be used within an ActiveKeysProvider');
|
|
||||||
}
|
|
||||||
return context;
|
|
||||||
};
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
export interface ProviderResponse {
|
|
||||||
supported: boolean;
|
|
||||||
name?: string;
|
|
||||||
description?: string;
|
|
||||||
models?: string[];
|
|
||||||
config_status: Record<string, ConfigDetails>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ConfigDetails {
|
|
||||||
key: string;
|
|
||||||
is_set: boolean;
|
|
||||||
location?: string;
|
|
||||||
}
|
|
||||||
@@ -1,121 +0,0 @@
|
|||||||
import { ProviderResponse, ConfigDetails } from './types';
|
|
||||||
import { getApiUrl, getSecretKey } from '../../../config';
|
|
||||||
import { default_key_value, required_keys } from '../models/hardcoded_stuff'; // e.g. { OPENAI_HOST: '', OLLAMA_HOST: '' }
|
|
||||||
|
|
||||||
// Backend API response types
|
|
||||||
interface ProviderMetadata {
|
|
||||||
description: string;
|
|
||||||
models: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ProviderDetails {
|
|
||||||
name: string;
|
|
||||||
metadata: ProviderMetadata;
|
|
||||||
is_configured: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isSecretKey(keyName: string): boolean {
|
|
||||||
// Endpoints and hosts should not be stored as secrets
|
|
||||||
const nonSecretKeys = [
|
|
||||||
'ANTHROPIC_HOST',
|
|
||||||
'DATABRICKS_HOST',
|
|
||||||
'OLLAMA_HOST',
|
|
||||||
'OPENAI_HOST',
|
|
||||||
'OPENAI_BASE_PATH',
|
|
||||||
'AZURE_OPENAI_ENDPOINT',
|
|
||||||
'AZURE_OPENAI_DEPLOYMENT_NAME',
|
|
||||||
'AZURE_OPENAI_API_VERSION',
|
|
||||||
'GCP_PROJECT_ID',
|
|
||||||
'GCP_LOCATION',
|
|
||||||
];
|
|
||||||
return !nonSecretKeys.includes(keyName);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getActiveProviders(): Promise<string[]> {
|
|
||||||
try {
|
|
||||||
const configSettings = await getConfigSettings();
|
|
||||||
const activeProviders = Object.values(configSettings)
|
|
||||||
.filter((provider) => {
|
|
||||||
const providerName = provider.name;
|
|
||||||
const configStatus = provider.config_status ?? {};
|
|
||||||
|
|
||||||
// Skip if provider isn't in required_keys
|
|
||||||
if (!required_keys[providerName as keyof typeof required_keys]) return false;
|
|
||||||
|
|
||||||
// Get all required keys for this provider
|
|
||||||
const providerRequiredKeys = required_keys[providerName as keyof typeof required_keys];
|
|
||||||
|
|
||||||
// Special case: If a provider has exactly one required key and that key
|
|
||||||
// has a default value, check if it's explicitly set
|
|
||||||
if (providerRequiredKeys.length === 1 && providerRequiredKeys[0] in default_key_value) {
|
|
||||||
const key = providerRequiredKeys[0];
|
|
||||||
// Only consider active if the key is explicitly set
|
|
||||||
return configStatus[key]?.is_set === true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// For providers with multiple keys or keys without defaults:
|
|
||||||
// Check if all required keys without defaults are set
|
|
||||||
const requiredNonDefaultKeys = providerRequiredKeys.filter(
|
|
||||||
(key: string) => !(key in default_key_value)
|
|
||||||
);
|
|
||||||
|
|
||||||
// If there are no non-default keys, this provider needs at least one key explicitly set
|
|
||||||
if (requiredNonDefaultKeys.length === 0) {
|
|
||||||
return providerRequiredKeys.some((key: string) => configStatus[key]?.is_set === true);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Otherwise, all non-default keys must be set
|
|
||||||
return requiredNonDefaultKeys.every((key: string) => configStatus[key]?.is_set === true);
|
|
||||||
})
|
|
||||||
.map((provider) => provider.name || 'Unknown Provider');
|
|
||||||
|
|
||||||
console.log('[GET ACTIVE PROVIDERS]:', activeProviders);
|
|
||||||
return activeProviders;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to get active providers:', error);
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getConfigSettings(): Promise<Record<string, ProviderResponse>> {
|
|
||||||
// Fetch provider config status
|
|
||||||
const response = await fetch(getApiUrl('/config/providers'), {
|
|
||||||
method: 'GET',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'X-Secret-Key': getSecretKey(),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error('Failed to fetch provider configuration status');
|
|
||||||
}
|
|
||||||
|
|
||||||
const providers: ProviderDetails[] = await response.json();
|
|
||||||
|
|
||||||
// Convert the response to the expected format
|
|
||||||
const data: Record<string, ProviderResponse> = {};
|
|
||||||
providers.forEach((provider) => {
|
|
||||||
const providerRequiredKeys = required_keys[provider.name as keyof typeof required_keys] || [];
|
|
||||||
|
|
||||||
data[provider.name] = {
|
|
||||||
name: provider.name,
|
|
||||||
supported: true,
|
|
||||||
description: provider.metadata.description,
|
|
||||||
models: provider.metadata.models,
|
|
||||||
config_status: providerRequiredKeys.reduce<Record<string, ConfigDetails>>(
|
|
||||||
(acc: Record<string, ConfigDetails>, key: string) => {
|
|
||||||
acc[key] = {
|
|
||||||
key,
|
|
||||||
is_set: provider.is_configured,
|
|
||||||
location: provider.is_configured ? 'config' : undefined,
|
|
||||||
};
|
|
||||||
return acc;
|
|
||||||
},
|
|
||||||
{}
|
|
||||||
),
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
@@ -1,110 +0,0 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
|
||||||
import { Card } from '../../ui/card';
|
|
||||||
import { Button } from '../../ui/button';
|
|
||||||
import { GooseMode, ModeSelectionItem } from './ModeSelectionItem';
|
|
||||||
|
|
||||||
interface ConfigureApproveModeProps {
|
|
||||||
onClose: () => void;
|
|
||||||
handleModeChange: (newMode: string) => void;
|
|
||||||
currentMode: string | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ConfigureApproveMode({
|
|
||||||
onClose,
|
|
||||||
handleModeChange,
|
|
||||||
currentMode,
|
|
||||||
}: ConfigureApproveModeProps) {
|
|
||||||
const approveModes: GooseMode[] = [
|
|
||||||
{
|
|
||||||
key: 'approve',
|
|
||||||
label: 'Manual Approval',
|
|
||||||
description: 'All tools, extensions and file modifications will require human approval',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'smart_approve',
|
|
||||||
label: 'Smart Approval',
|
|
||||||
description: 'Intelligently determine which actions need approval based on risk level ',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
||||||
const [approveMode, setApproveMode] = useState(currentMode);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setApproveMode(currentMode);
|
|
||||||
}, [currentMode]);
|
|
||||||
|
|
||||||
const handleModeSubmit = async (e: React.FormEvent) => {
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
setIsSubmitting(true);
|
|
||||||
try {
|
|
||||||
handleModeChange(approveMode || '');
|
|
||||||
onClose();
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error configuring goose mode:', error);
|
|
||||||
} finally {
|
|
||||||
setIsSubmitting(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="fixed inset-0 bg-black/20 backdrop-blur-sm z-10">
|
|
||||||
<Card className="fixed top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[440px] bg-white dark:bg-gray-800 rounded-xl shadow-xl overflow-hidden p-0">
|
|
||||||
<div className="px-4 pb-0 space-y-6">
|
|
||||||
<div className="p-[16px] pt-[24px] pb-0">
|
|
||||||
{/* Header */}
|
|
||||||
<div className="flex">
|
|
||||||
<h2 className="text-2xl font-regular dark:text-white text-gray-900">
|
|
||||||
Configure Approve Mode
|
|
||||||
</h2>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mt-[24px]">
|
|
||||||
<p className="text-sm text-gray-500 dark:text-gray-400 mb-6">
|
|
||||||
Approve requests can either be given to all tool requests or determine which actions
|
|
||||||
may need integration
|
|
||||||
</p>
|
|
||||||
<div className="space-y-4">
|
|
||||||
{approveModes.map((mode) => (
|
|
||||||
<ModeSelectionItem
|
|
||||||
key={mode.key}
|
|
||||||
mode={mode}
|
|
||||||
showDescription={true}
|
|
||||||
currentMode={approveMode || ''}
|
|
||||||
isApproveModeConfigure={true}
|
|
||||||
handleModeChange={(newMode) => {
|
|
||||||
setApproveMode(newMode);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Actions */}
|
|
||||||
<div className="mt-[8px] ml-[-24px] mr-[-24px] pt-[16px]">
|
|
||||||
<Button
|
|
||||||
type="submit"
|
|
||||||
variant="ghost"
|
|
||||||
disabled={isSubmitting}
|
|
||||||
onClick={handleModeSubmit}
|
|
||||||
className="w-full h-[60px] rounded-none border-t text-lg hover:bg-gray-50 dark:hover:text-black dark:text-white dark:border-gray-600 font-regular"
|
|
||||||
>
|
|
||||||
{isSubmitting ? 'Saving...' : 'Save Mode'}
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant="ghost"
|
|
||||||
disabled={isSubmitting}
|
|
||||||
onClick={onClose}
|
|
||||||
className="w-full h-[60px] rounded-none border-t text-gray-400 dark:hover:text-black hover:bg-gray-50 dark:border-gray-600 text-lg font-regular"
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
import { useEffect, useState, useCallback } from 'react';
|
|
||||||
import { all_goose_modes, filterGooseModes, ModeSelectionItem } from './ModeSelectionItem';
|
|
||||||
import { useConfig } from '../../ConfigContext';
|
|
||||||
|
|
||||||
export const ModeSelection = () => {
|
|
||||||
const [currentMode, setCurrentMode] = useState('auto');
|
|
||||||
const [previousApproveModel, setPreviousApproveModel] = useState('');
|
|
||||||
const { read, upsert } = useConfig();
|
|
||||||
|
|
||||||
const handleModeChange = async (newMode: string) => {
|
|
||||||
try {
|
|
||||||
await upsert('GOOSE_MODE', newMode, false);
|
|
||||||
// Only track the previous approve if current mode is approve related but new mode is not.
|
|
||||||
if (currentMode.includes('approve') && !newMode.includes('approve')) {
|
|
||||||
setPreviousApproveModel(currentMode);
|
|
||||||
}
|
|
||||||
setCurrentMode(newMode);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error updating goose mode:', error);
|
|
||||||
throw new Error(`Failed to store new goose mode: ${newMode}`);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const fetchCurrentMode = useCallback(async () => {
|
|
||||||
try {
|
|
||||||
const mode = (await read('GOOSE_MODE', false)) as string;
|
|
||||||
if (mode) {
|
|
||||||
setCurrentMode(mode);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error fetching current mode:', error);
|
|
||||||
}
|
|
||||||
}, [read]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
fetchCurrentMode();
|
|
||||||
}, [fetchCurrentMode]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<div>
|
|
||||||
{filterGooseModes(currentMode, all_goose_modes, previousApproveModel).map((mode) => (
|
|
||||||
<ModeSelectionItem
|
|
||||||
key={mode.key}
|
|
||||||
mode={mode}
|
|
||||||
currentMode={currentMode}
|
|
||||||
showDescription={true}
|
|
||||||
isApproveModeConfigure={false}
|
|
||||||
handleModeChange={handleModeChange}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,143 +0,0 @@
|
|||||||
import { useEffect, useState } from 'react';
|
|
||||||
import { Gear } from '../../icons';
|
|
||||||
import { ConfigureApproveMode } from './ConfigureApproveMode';
|
|
||||||
|
|
||||||
export interface GooseMode {
|
|
||||||
key: string;
|
|
||||||
label: string;
|
|
||||||
description: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const all_goose_modes: GooseMode[] = [
|
|
||||||
{
|
|
||||||
key: 'auto',
|
|
||||||
label: 'Autonomous',
|
|
||||||
description: 'Full file modification capabilities, edit, create, and delete files freely.',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'approve',
|
|
||||||
label: 'Manual Approval',
|
|
||||||
description: 'All tools, extensions and file modifications will require human approval',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'smart_approve',
|
|
||||||
label: 'Smart Approval',
|
|
||||||
description: 'Intelligently determine which actions need approval based on risk level ',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'chat',
|
|
||||||
label: 'Chat Only',
|
|
||||||
description: 'Engage with the selected provider without using tools or extensions.',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
export function filterGooseModes(
|
|
||||||
currentMode: string,
|
|
||||||
modes: GooseMode[],
|
|
||||||
previousApproveMode: string
|
|
||||||
) {
|
|
||||||
return modes.filter((mode) => {
|
|
||||||
const approveList = ['approve', 'smart_approve'];
|
|
||||||
const nonApproveList = ['auto', 'chat'];
|
|
||||||
// Always keep 'auto' and 'chat'
|
|
||||||
if (nonApproveList.includes(mode.key)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
// If current mode is non approve mode, we display write approve by default.
|
|
||||||
if (nonApproveList.includes(currentMode) && !previousApproveMode) {
|
|
||||||
return mode.key === 'smart_approve';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Always include the current and previou approve mode
|
|
||||||
if (mode.key === currentMode) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Current mode and previous approve mode cannot exist at the same time.
|
|
||||||
if (approveList.includes(currentMode) && approveList.includes(previousApproveMode)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mode.key === previousApproveMode) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ModeSelectionItemProps {
|
|
||||||
currentMode: string;
|
|
||||||
mode: GooseMode;
|
|
||||||
showDescription: boolean;
|
|
||||||
isApproveModeConfigure: boolean;
|
|
||||||
handleModeChange: (newMode: string) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ModeSelectionItem({
|
|
||||||
currentMode,
|
|
||||||
mode,
|
|
||||||
showDescription,
|
|
||||||
isApproveModeConfigure,
|
|
||||||
handleModeChange,
|
|
||||||
}: ModeSelectionItemProps) {
|
|
||||||
const [checked, setChecked] = useState(currentMode == mode.key);
|
|
||||||
const [isDislogOpen, setIsDislogOpen] = useState(false);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setChecked(currentMode === mode.key);
|
|
||||||
}, [currentMode, mode.key]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<div className="group hover:cursor-pointer" onClick={() => handleModeChange(mode.key)}>
|
|
||||||
<div className="flex items-center justify-between text-textStandard mb-4">
|
|
||||||
<div className="flex">
|
|
||||||
<h3 className="text-textStandard">{mode.label}</h3>
|
|
||||||
{showDescription && (
|
|
||||||
<p className="text-xs text-textSubtle mt-[2px]">{mode.description}</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="relative flex items-center gap-3">
|
|
||||||
{!isApproveModeConfigure && (mode.key == 'approve' || mode.key == 'smart_approve') && (
|
|
||||||
<button
|
|
||||||
onClick={() => {
|
|
||||||
setIsDislogOpen(true);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Gear className="w-5 h-5 text-textSubtle hover:text-textStandard" />
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
<input
|
|
||||||
type="radio"
|
|
||||||
name="modes"
|
|
||||||
value={mode.key}
|
|
||||||
checked={checked}
|
|
||||||
onChange={() => handleModeChange(mode.key)}
|
|
||||||
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>
|
|
||||||
{isDislogOpen ? (
|
|
||||||
<ConfigureApproveMode
|
|
||||||
onClose={() => {
|
|
||||||
setIsDislogOpen(false);
|
|
||||||
}}
|
|
||||||
handleModeChange={handleModeChange}
|
|
||||||
currentMode={currentMode}
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,171 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { Card } from '../../ui/card';
|
|
||||||
import { Button } from '../../ui/button';
|
|
||||||
import { Input } from '../../ui/input';
|
|
||||||
import { FullExtensionConfig } from '../../../extensions';
|
|
||||||
import { getApiUrl, getSecretKey } from '../../../config';
|
|
||||||
import { addExtension } from '../../../extensions';
|
|
||||||
import { toastError, toastSuccess } from '../../../toasts';
|
|
||||||
|
|
||||||
interface ConfigureExtensionModalProps {
|
|
||||||
isOpen: boolean;
|
|
||||||
onClose: () => void;
|
|
||||||
onSubmit: () => void;
|
|
||||||
extension: FullExtensionConfig | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ConfigureBuiltInExtensionModal({
|
|
||||||
isOpen,
|
|
||||||
onClose,
|
|
||||||
onSubmit,
|
|
||||||
extension,
|
|
||||||
}: ConfigureExtensionModalProps) {
|
|
||||||
const [envValues, setEnvValues] = React.useState<Record<string, string>>({});
|
|
||||||
const [isSubmitting, setIsSubmitting] = React.useState(false);
|
|
||||||
|
|
||||||
// Reset form when dialog closes or extension changes
|
|
||||||
React.useEffect(() => {
|
|
||||||
if (!isOpen || !extension) {
|
|
||||||
setEnvValues({});
|
|
||||||
}
|
|
||||||
}, [isOpen, extension]);
|
|
||||||
|
|
||||||
const handleExtensionConfigSubmit = async (e: React.FormEvent) => {
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
if (!extension) return;
|
|
||||||
|
|
||||||
setIsSubmitting(true);
|
|
||||||
try {
|
|
||||||
// First store all environment variables
|
|
||||||
if (extension.env_keys && extension.env_keys.length > 0) {
|
|
||||||
for (const envKey of extension.env_keys) {
|
|
||||||
const value = envValues[envKey];
|
|
||||||
if (!value) continue;
|
|
||||||
|
|
||||||
const storeResponse = await fetch(getApiUrl('/configs/store'), {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'X-Secret-Key': getSecretKey(),
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
key: envKey,
|
|
||||||
value: value.trim(),
|
|
||||||
isSecret: true,
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!storeResponse.ok) {
|
|
||||||
throw new Error(`Failed to store environment variable: ${envKey}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await addExtension(extension);
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error('Failed to add system configuration');
|
|
||||||
}
|
|
||||||
|
|
||||||
toastSuccess({
|
|
||||||
title: extension.name,
|
|
||||||
msg: `Successfully configured extension`,
|
|
||||||
});
|
|
||||||
onSubmit();
|
|
||||||
onClose();
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Error configuring extension:', err);
|
|
||||||
const errorMessage = err instanceof Error ? err.message : 'Unknown error occurred';
|
|
||||||
toastError({
|
|
||||||
title: extension.name,
|
|
||||||
msg: `Failed to configure the extension`,
|
|
||||||
traceback: errorMessage,
|
|
||||||
});
|
|
||||||
} finally {
|
|
||||||
setIsSubmitting(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!extension || !isOpen) return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="fixed inset-0 bg-black/20 backdrop-blur-sm">
|
|
||||||
<Card className="fixed top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[440px] bg-white dark:bg-gray-800 rounded-xl shadow-xl overflow-hidden p-[16px] pt-[24px] pb-0">
|
|
||||||
<div className="px-8 pb-0 space-y-8">
|
|
||||||
{/* Header */}
|
|
||||||
<div className="flex">
|
|
||||||
<h2 className="text-2xl font-regular dark:text-white text-gray-900">
|
|
||||||
Configure {extension.name}
|
|
||||||
</h2>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Form */}
|
|
||||||
<form onSubmit={handleExtensionConfigSubmit}>
|
|
||||||
<div className="mt-[24px]">
|
|
||||||
{extension.env_keys && extension.env_keys.length > 0 ? (
|
|
||||||
<>
|
|
||||||
<p className="text-sm text-gray-500 dark:text-gray-400 mb-6">
|
|
||||||
Please provide the required environment variables for this extension:
|
|
||||||
</p>
|
|
||||||
<div className="space-y-4">
|
|
||||||
{extension.env_keys.map((envVarName) => (
|
|
||||||
<div key={envVarName}>
|
|
||||||
<label
|
|
||||||
htmlFor={envVarName}
|
|
||||||
className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2"
|
|
||||||
>
|
|
||||||
{envVarName}
|
|
||||||
</label>
|
|
||||||
<Input
|
|
||||||
type="text"
|
|
||||||
id={envVarName}
|
|
||||||
name={envVarName}
|
|
||||||
placeholder={envVarName}
|
|
||||||
value={envValues[envVarName] || ''}
|
|
||||||
onChange={(e) =>
|
|
||||||
setEnvValues((prev) => ({
|
|
||||||
...prev,
|
|
||||||
[envVarName]: e.target.value,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
className="w-full h-14 px-4 font-regular rounded-lg border shadow-none border-gray-300 bg-white text-lg placeholder:text-gray-400 font-regular text-gray-900 dark:bg-gray-800 dark:border-gray-600 dark:text-white dark:placeholder:text-gray-500"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<p className="text-sm text-gray-500 dark:text-gray-400">
|
|
||||||
This extension doesn't require any environment variables.
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Actions */}
|
|
||||||
<div className="mt-[8px] ml-[-24px] mr-[-24px] pt-[16px]">
|
|
||||||
<Button
|
|
||||||
type="submit"
|
|
||||||
variant="ghost"
|
|
||||||
disabled={isSubmitting}
|
|
||||||
className="w-full h-[60px] rounded-none border-t dark:border-gray-600 text-indigo-500 hover:bg-indigo-50 dark:hover:bg-indigo-900/20 dark:border-gray-600 text-lg font-regular"
|
|
||||||
>
|
|
||||||
{isSubmitting ? 'Saving...' : 'Save Configuration'}
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant="ghost"
|
|
||||||
onClick={onClose}
|
|
||||||
disabled={isSubmitting}
|
|
||||||
className="w-full h-[60px] rounded-none border-t dark:border-gray-600 text-gray-400 hover:bg-gray-50 dark:border-gray-600 text-lg font-regular"
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,182 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { Card } from '../../ui/card';
|
|
||||||
import { Button } from '../../ui/button';
|
|
||||||
import { Input } from '../../ui/input';
|
|
||||||
import { FullExtensionConfig } from '../../../extensions';
|
|
||||||
import { getApiUrl, getSecretKey } from '../../../config';
|
|
||||||
import { addExtension } from '../../../extensions';
|
|
||||||
import { toastError, toastSuccess } from '../../../toasts';
|
|
||||||
|
|
||||||
interface ConfigureExtensionModalProps {
|
|
||||||
isOpen: boolean;
|
|
||||||
onClose: () => void;
|
|
||||||
onSubmit: () => void;
|
|
||||||
onRemove: () => void;
|
|
||||||
extension: FullExtensionConfig | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ConfigureExtensionModal({
|
|
||||||
isOpen,
|
|
||||||
onClose,
|
|
||||||
onSubmit,
|
|
||||||
onRemove,
|
|
||||||
extension,
|
|
||||||
}: ConfigureExtensionModalProps) {
|
|
||||||
const [envValues, setEnvValues] = React.useState<Record<string, string>>({});
|
|
||||||
const [isSubmitting, setIsSubmitting] = React.useState(false);
|
|
||||||
|
|
||||||
// Reset form when dialog closes or extension changes
|
|
||||||
React.useEffect(() => {
|
|
||||||
if (!isOpen || !extension) {
|
|
||||||
setEnvValues({});
|
|
||||||
}
|
|
||||||
}, [isOpen, extension]);
|
|
||||||
|
|
||||||
const handleExtensionConfigSubmit = async (e: React.FormEvent) => {
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
if (!extension) return;
|
|
||||||
|
|
||||||
setIsSubmitting(true);
|
|
||||||
try {
|
|
||||||
// First store all environment variables
|
|
||||||
if (extension.env_keys && extension.env_keys.length > 0) {
|
|
||||||
for (const envKey of extension.env_keys) {
|
|
||||||
const value = envValues[envKey];
|
|
||||||
if (!value) continue;
|
|
||||||
|
|
||||||
const storeResponse = await fetch(getApiUrl('/configs/store'), {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'X-Secret-Key': getSecretKey(),
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
key: envKey,
|
|
||||||
value: value.trim(),
|
|
||||||
isSecret: true,
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!storeResponse.ok) {
|
|
||||||
throw new Error(`Failed to store environment variable: ${envKey}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await addExtension(extension);
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error('Failed to add system configuration');
|
|
||||||
}
|
|
||||||
|
|
||||||
toastSuccess({
|
|
||||||
title: extension.name,
|
|
||||||
msg: `Successfully configured extension`,
|
|
||||||
});
|
|
||||||
onSubmit();
|
|
||||||
onClose();
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Error configuring extension:', err);
|
|
||||||
const errorMessage = err instanceof Error ? err.message : 'Unknown error occurred';
|
|
||||||
toastError({
|
|
||||||
title: extension.name,
|
|
||||||
msg: `Failed to configure extension`,
|
|
||||||
traceback: errorMessage,
|
|
||||||
});
|
|
||||||
} finally {
|
|
||||||
setIsSubmitting(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!extension || !isOpen) return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="fixed inset-0 bg-black/20 backdrop-blur-sm">
|
|
||||||
<Card className="fixed top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[440px] bg-white dark:bg-gray-800 rounded-xl shadow-xl overflow-hidden p-[16px] pt-[24px] pb-0">
|
|
||||||
<div className="px-8 pb-0 space-y-8">
|
|
||||||
{/* Header */}
|
|
||||||
<div className="flex">
|
|
||||||
<h2 className="text-2xl font-regular dark:text-white text-gray-900">
|
|
||||||
Configure {extension.name}
|
|
||||||
</h2>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Form */}
|
|
||||||
<form onSubmit={handleExtensionConfigSubmit}>
|
|
||||||
<div className="mt-[24px]">
|
|
||||||
{extension.env_keys && extension.env_keys.length > 0 ? (
|
|
||||||
<>
|
|
||||||
<p className="text-sm text-gray-500 dark:text-gray-400 mb-6">
|
|
||||||
Please provide the required environment variables for this extension:
|
|
||||||
</p>
|
|
||||||
<div className="space-y-4">
|
|
||||||
{extension.env_keys.map((envVarName) => (
|
|
||||||
<div key={envVarName}>
|
|
||||||
<label
|
|
||||||
htmlFor={envVarName}
|
|
||||||
className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2"
|
|
||||||
>
|
|
||||||
{envVarName}
|
|
||||||
</label>
|
|
||||||
<Input
|
|
||||||
type="text"
|
|
||||||
id={envVarName}
|
|
||||||
name={envVarName}
|
|
||||||
placeholder={envVarName}
|
|
||||||
value={envValues[envVarName] || ''}
|
|
||||||
onChange={(e) =>
|
|
||||||
setEnvValues((prev) => ({
|
|
||||||
...prev,
|
|
||||||
[envVarName]: e.target.value,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
className="w-full h-14 px-4 font-regular rounded-lg border shadow-none border-gray-300 bg-white text-lg placeholder:text-gray-400 font-regular text-gray-900 dark:bg-gray-800 dark:border-gray-600 dark:text-white dark:placeholder:text-gray-500"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<p className="text-sm text-gray-500 dark:text-gray-400">
|
|
||||||
This extension doesn't require any environment variables.
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Actions */}
|
|
||||||
<div className="mt-[8px] ml-[-24px] mr-[-24px] pt-[16px]">
|
|
||||||
<Button
|
|
||||||
type="submit"
|
|
||||||
variant="ghost"
|
|
||||||
disabled={isSubmitting}
|
|
||||||
className="w-full h-[60px] rounded-none border-t dark:border-gray-600 text-lg hover:bg-gray-50 hover:dark:text-black dark:text-white dark:border-gray-600 font-regular"
|
|
||||||
>
|
|
||||||
{isSubmitting ? 'Saving...' : 'Save Configuration'}
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant="ghost"
|
|
||||||
onClick={onRemove}
|
|
||||||
disabled={isSubmitting}
|
|
||||||
className="w-full h-[60px] rounded-none border-t dark:border-gray-600 text-red-600 hover:bg-red-50 dark:hover:bg-red-900/20 dark:border-gray-600 text-lg font-regular"
|
|
||||||
>
|
|
||||||
Remove Extension
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant="ghost"
|
|
||||||
onClick={onClose}
|
|
||||||
disabled={isSubmitting}
|
|
||||||
className="w-full h-[60px] rounded-none border-t dark:border-gray-600 text-gray-400 hover:bg-gray-50 dark:border-gray-600 text-lg font-regular"
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { FullExtensionConfig } from '../../../extensions';
|
|
||||||
import { Gear } from '../../icons';
|
|
||||||
|
|
||||||
type ExtensionItemProps = FullExtensionConfig & {
|
|
||||||
onToggle: (id: string) => void;
|
|
||||||
onConfigure: (extension: FullExtensionConfig) => void;
|
|
||||||
canConfigure?: boolean; // Added optional prop here
|
|
||||||
};
|
|
||||||
|
|
||||||
export const ExtensionItem: React.FC<ExtensionItemProps> = (props) => {
|
|
||||||
const { id, name, description, enabled, onToggle, onConfigure, canConfigure } = props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="rounded-lg border border-borderSubtle p-4">
|
|
||||||
<div className="flex justify-between items-center">
|
|
||||||
<div className="">
|
|
||||||
<div className="flex items-center">
|
|
||||||
<h3 className="text-sm font-semibold text-textStandard">{name}</h3>
|
|
||||||
</div>
|
|
||||||
<p className="text-xs text-textSubtle mt-[2px]">{description}</p>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
{canConfigure && ( // Conditionally render the gear icon
|
|
||||||
<button onClick={() => onConfigure(props)} className="">
|
|
||||||
<Gear className="w-5 h-5 text-textSubtle hover:text-textStandard" />
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
<button
|
|
||||||
onClick={() => onToggle(id)}
|
|
||||||
className={`relative inline-flex h-6 w-11 items-center rounded-full ${
|
|
||||||
enabled ? 'bg-indigo-500' : 'bg-bgProminent'
|
|
||||||
} transition-colors duration-200 ease-in-out focus:outline-none`}
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
className={`inline-block h-5 w-5 transform rounded-full bg-white shadow ${
|
|
||||||
enabled ? 'translate-x-[22px]' : 'translate-x-[2px]'
|
|
||||||
} transition-transform duration-200 ease-in-out`}
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,315 +0,0 @@
|
|||||||
import React, { useState } from 'react';
|
|
||||||
import { Card } from '../../ui/card';
|
|
||||||
import { Button } from '../../ui/button';
|
|
||||||
import { Input } from '../../ui/input';
|
|
||||||
import { FullExtensionConfig, DEFAULT_EXTENSION_TIMEOUT } from '../../../extensions';
|
|
||||||
import { Select } from '../../ui/Select';
|
|
||||||
import { getApiUrl, getSecretKey } from '../../../config';
|
|
||||||
import { toastError } from '../../../toasts';
|
|
||||||
|
|
||||||
interface ManualExtensionModalProps {
|
|
||||||
isOpen: boolean;
|
|
||||||
onClose: () => void;
|
|
||||||
onSubmit: (extension: FullExtensionConfig) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ManualExtensionModal({ isOpen, onClose, onSubmit }: ManualExtensionModalProps) {
|
|
||||||
const [formData, setFormData] = useState<
|
|
||||||
Partial<FullExtensionConfig> & { commandInput?: string }
|
|
||||||
>({
|
|
||||||
type: 'stdio',
|
|
||||||
enabled: true,
|
|
||||||
args: [],
|
|
||||||
commandInput: '',
|
|
||||||
timeout: DEFAULT_EXTENSION_TIMEOUT,
|
|
||||||
});
|
|
||||||
const [envKey, setEnvKey] = useState('');
|
|
||||||
const [envValue, setEnvValue] = useState('');
|
|
||||||
const [envVars, setEnvVars] = useState<Array<{ key: string; value: string }>>([]);
|
|
||||||
|
|
||||||
const typeOptions = [
|
|
||||||
{ value: 'stdio', label: 'Standard IO' },
|
|
||||||
{ value: 'sse', label: 'Server-Sent Events' },
|
|
||||||
{ value: 'builtin', label: 'Built-in' },
|
|
||||||
];
|
|
||||||
|
|
||||||
const handleSubmit = async (e: React.FormEvent) => {
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
if (!formData.id || !formData.name || !formData.description) {
|
|
||||||
toastError({ title: 'Please fill in all required fields' });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (formData.type === 'stdio' && !formData.commandInput) {
|
|
||||||
toastError({ title: 'Command is required for stdio type' });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (formData.type === 'sse' && !formData.uri) {
|
|
||||||
toastError({ title: 'URI is required for SSE type' });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (formData.type === 'builtin' && !formData.name) {
|
|
||||||
toastError({ title: 'Name is required for builtin type' });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Store environment variables as secrets
|
|
||||||
for (const envVar of envVars) {
|
|
||||||
const storeResponse = await fetch(getApiUrl('/configs/store'), {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'X-Secret-Key': getSecretKey(),
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
key: envVar.key,
|
|
||||||
value: envVar.value.trim(),
|
|
||||||
isSecret: true,
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!storeResponse.ok) {
|
|
||||||
throw new Error(`Failed to store environment variable: ${envVar.key}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse command input into cmd and args
|
|
||||||
let cmd = '';
|
|
||||||
let args: string[] = [];
|
|
||||||
if (formData.type === 'stdio' && formData.commandInput) {
|
|
||||||
const parts = formData.commandInput.trim().split(/\s+/);
|
|
||||||
[cmd, ...args] = parts;
|
|
||||||
}
|
|
||||||
|
|
||||||
const extension: FullExtensionConfig = {
|
|
||||||
...formData,
|
|
||||||
type: formData.type!,
|
|
||||||
enabled: true,
|
|
||||||
env_keys: envVars.map((v) => v.key),
|
|
||||||
...(formData.type === 'stdio' && { cmd, args }),
|
|
||||||
} as FullExtensionConfig;
|
|
||||||
|
|
||||||
onSubmit(extension);
|
|
||||||
resetForm();
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error configuring extension:', error);
|
|
||||||
toastError({
|
|
||||||
title: 'Failed to configure extension',
|
|
||||||
traceback: error instanceof Error ? error.message : String(error),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const resetForm = () => {
|
|
||||||
setFormData({
|
|
||||||
type: 'stdio',
|
|
||||||
enabled: true,
|
|
||||||
args: [],
|
|
||||||
commandInput: '',
|
|
||||||
});
|
|
||||||
setEnvVars([]);
|
|
||||||
setEnvKey('');
|
|
||||||
setEnvValue('');
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleAddEnvVar = () => {
|
|
||||||
if (envKey && !envVars.some((v) => v.key === envKey)) {
|
|
||||||
setEnvVars([...envVars, { key: envKey, value: envValue }]);
|
|
||||||
setEnvKey('');
|
|
||||||
setEnvValue('');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleRemoveEnvVar = (key: string) => {
|
|
||||||
setEnvVars(envVars.filter((v) => v.key !== key));
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!isOpen) return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="fixed flex items-center justify-center inset-0 bg-black/20 dark:bg-white/20 backdrop-blur-sm">
|
|
||||||
<Card className="w-[500px] bg-bgApp rounded-xl overflow-hidden shadow-none p-[16px] pt-[24px] pb-0 max-h-[90vh] overflow-y-auto">
|
|
||||||
<div className="px-4 pb-0 space-y-8">
|
|
||||||
<div className="flex">
|
|
||||||
<h2 className="text-2xl font-regular text-textStandard">Add custom extension</h2>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<form onSubmit={handleSubmit}>
|
|
||||||
<div className="space-y-4">
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-textStandard mb-2">Type</label>
|
|
||||||
<Select
|
|
||||||
options={typeOptions}
|
|
||||||
value={typeOptions.find((option) => option.value === formData.type)}
|
|
||||||
onChange={(newValue: unknown) => {
|
|
||||||
const option = newValue as { value: string; label: string } | null;
|
|
||||||
setFormData({
|
|
||||||
...formData,
|
|
||||||
type: option?.value as FullExtensionConfig['type'],
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-textStandard mb-2">ID *</label>
|
|
||||||
<Input
|
|
||||||
type="text"
|
|
||||||
value={formData.id || ''}
|
|
||||||
onChange={(e) => setFormData({ ...formData, id: e.target.value })}
|
|
||||||
className="w-full"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-textStandard mb-2">Name *</label>
|
|
||||||
<Input
|
|
||||||
type="text"
|
|
||||||
value={formData.name || ''}
|
|
||||||
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
|
|
||||||
className="w-full"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-textStandard mb-2">
|
|
||||||
Description *
|
|
||||||
</label>
|
|
||||||
<Input
|
|
||||||
type="text"
|
|
||||||
value={formData.description || ''}
|
|
||||||
onChange={(e) => setFormData({ ...formData, description: e.target.value })}
|
|
||||||
className="w-full"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{formData.type === 'stdio' && (
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-textStandard mb-2">
|
|
||||||
Command * (command and arguments separated by spaces)
|
|
||||||
</label>
|
|
||||||
<Input
|
|
||||||
type="text"
|
|
||||||
value={formData.commandInput || ''}
|
|
||||||
onChange={(e) => setFormData({ ...formData, commandInput: e.target.value })}
|
|
||||||
placeholder="e.g. goosed mcp example"
|
|
||||||
className="w-full"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{formData.type === 'sse' && (
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-textStandard mb-2">URI *</label>
|
|
||||||
<Input
|
|
||||||
type="text"
|
|
||||||
value={formData.uri || ''}
|
|
||||||
onChange={(e) => setFormData({ ...formData, uri: e.target.value })}
|
|
||||||
className="w-full"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-textStandard mb-2">
|
|
||||||
Environment Variables
|
|
||||||
</label>
|
|
||||||
<div className="flex gap-2 mb-2">
|
|
||||||
<Input
|
|
||||||
type="text"
|
|
||||||
value={envKey}
|
|
||||||
onChange={(e) => setEnvKey(e.target.value)}
|
|
||||||
placeholder="Environment variable name"
|
|
||||||
className="flex-1"
|
|
||||||
/>
|
|
||||||
<Input
|
|
||||||
type="text"
|
|
||||||
value={envValue}
|
|
||||||
onChange={(e) => setEnvValue(e.target.value)}
|
|
||||||
placeholder="Value"
|
|
||||||
className="flex-1"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
onClick={handleAddEnvVar}
|
|
||||||
className="bg-bgApp hover:bg-bgApp shadow-none border border-borderSubtle hover:border-borderStandard transition-colors text-textStandard"
|
|
||||||
>
|
|
||||||
Add
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
{envVars.length > 0 && (
|
|
||||||
<div className="space-y-2">
|
|
||||||
{envVars.map((envVar) => (
|
|
||||||
<div key={envVar.key} className="flex items-center justify-between gap-1">
|
|
||||||
<div className="flex-1 whitespace-nowrap overflow-x-auto bg-gray-100 dark:bg-gray-700 p-2 rounded">
|
|
||||||
<div className="flex-1">
|
|
||||||
<span className="text-sm font-medium">{envVar.key}</span>
|
|
||||||
<span className="text-sm text-gray-500 dark:text-gray-400 ml-2">
|
|
||||||
= {envVar.value}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="bg-gray-100 dark:bg-gray-700 p-2 rounded">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => handleRemoveEnvVar(envVar.key)}
|
|
||||||
className="text-red-500 hover:text-red-700 ml-2"
|
|
||||||
>
|
|
||||||
Remove
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-textStandard mb-2">
|
|
||||||
Timeout (secs)*
|
|
||||||
</label>
|
|
||||||
<Input
|
|
||||||
type="number"
|
|
||||||
value={formData.timeout || DEFAULT_EXTENSION_TIMEOUT}
|
|
||||||
onChange={(e) => setFormData({ ...formData, timeout: parseInt(e.target.value) })}
|
|
||||||
className="w-full"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="mt-[8px] -ml-8 -mr-8 pt-8">
|
|
||||||
<Button
|
|
||||||
type="submit"
|
|
||||||
variant="ghost"
|
|
||||||
className="w-full h-[60px] rounded-none border-t border-borderSubtle text-md hover:bg-bgSubtle text-textProminent font-regular"
|
|
||||||
>
|
|
||||||
Add
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant="ghost"
|
|
||||||
onClick={() => {
|
|
||||||
resetForm();
|
|
||||||
onClose();
|
|
||||||
}}
|
|
||||||
className="w-full h-[60px] rounded-none border-t border-borderSubtle hover:text-textStandard text-textSubtle hover:bg-bgSubtle text-md font-regular"
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -184,3 +184,18 @@ export function removeShims(cmd: string) {
|
|||||||
// If it's not a shim, return the original command
|
// If it's not a shim, return the original command
|
||||||
return cmd;
|
return cmd;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function extractCommand(link: string): string {
|
||||||
|
const url = new URL(link);
|
||||||
|
const cmd = url.searchParams.get('cmd') || 'Unknown Command';
|
||||||
|
const args = url.searchParams.getAll('arg').map(decodeURIComponent);
|
||||||
|
|
||||||
|
// Combine the command and its arguments into a reviewable format
|
||||||
|
return `${cmd} ${args.join(' ')}`.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function extractExtensionName(link: string): string {
|
||||||
|
const url = new URL(link);
|
||||||
|
const name = url.searchParams.get('name');
|
||||||
|
return name ? decodeURIComponent(name) : 'Unknown Extension';
|
||||||
|
}
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
export function extractCommand(link: string): string {
|
|
||||||
const url = new URL(link);
|
|
||||||
const cmd = url.searchParams.get('cmd') || 'Unknown Command';
|
|
||||||
const args = url.searchParams.getAll('arg').map(decodeURIComponent);
|
|
||||||
|
|
||||||
// Combine the command and its arguments into a reviewable format
|
|
||||||
return `${cmd} ${args.join(' ')}`.trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
export function extractExtensionName(link: string): string {
|
|
||||||
const url = new URL(link);
|
|
||||||
const name = url.searchParams.get('name');
|
|
||||||
return name ? decodeURIComponent(name) : 'Unknown Extension';
|
|
||||||
}
|
|
||||||
@@ -1,129 +0,0 @@
|
|||||||
import { useState, useEffect } from 'react';
|
|
||||||
import { Button } from '../../ui/button';
|
|
||||||
import { Input } from '../../ui/input';
|
|
||||||
import Select from 'react-select';
|
|
||||||
import { Plus } from 'lucide-react';
|
|
||||||
import { createSelectedModel, useHandleModelSelection } from './utils';
|
|
||||||
import { useActiveKeys } from '../api_keys/ActiveKeysContext';
|
|
||||||
import { gooseModels } from './GooseModels';
|
|
||||||
import { createDarkSelectStyles, darkSelectTheme } from '../../ui/select-styles';
|
|
||||||
|
|
||||||
export function AddModelInline() {
|
|
||||||
const { activeKeys } = useActiveKeys(); // Access active keys from context
|
|
||||||
|
|
||||||
// Convert active keys to dropdown options
|
|
||||||
const providerOptions = activeKeys.map((key) => ({
|
|
||||||
value: key.toLowerCase(),
|
|
||||||
label: key,
|
|
||||||
}));
|
|
||||||
|
|
||||||
const [selectedProvider, setSelectedProvider] = useState<string | null>(null);
|
|
||||||
const [modelName, setModelName] = useState<string>('');
|
|
||||||
const [filteredModels, setFilteredModels] = useState<
|
|
||||||
{ id: string; name: string; provider: string }[]
|
|
||||||
>([]);
|
|
||||||
const [showSuggestions, setShowSuggestions] = useState(false);
|
|
||||||
const handleModelSelection = useHandleModelSelection();
|
|
||||||
|
|
||||||
// Filter models by selected provider and input text
|
|
||||||
useEffect(() => {
|
|
||||||
if (!selectedProvider || !modelName) {
|
|
||||||
setFilteredModels([]);
|
|
||||||
setShowSuggestions(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const filtered = gooseModels
|
|
||||||
.filter(
|
|
||||||
(model) =>
|
|
||||||
model.provider.toLowerCase() === selectedProvider &&
|
|
||||||
model.name.toLowerCase().includes(modelName.toLowerCase())
|
|
||||||
)
|
|
||||||
.slice(0, 5) // Limit suggestions to top 5
|
|
||||||
.map((model) => ({
|
|
||||||
id: String(model.id || ''),
|
|
||||||
name: model.name,
|
|
||||||
provider: model.provider,
|
|
||||||
}));
|
|
||||||
setFilteredModels(filtered);
|
|
||||||
setShowSuggestions(filtered.length > 0);
|
|
||||||
}, [modelName, selectedProvider]);
|
|
||||||
|
|
||||||
const handleSubmit = () => {
|
|
||||||
if (!selectedProvider || !modelName) {
|
|
||||||
console.error('Both provider and model name are required.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find the selected model from the filtered models
|
|
||||||
const selectedModel = createSelectedModel(selectedProvider, modelName);
|
|
||||||
|
|
||||||
// Trigger the model selection logic
|
|
||||||
handleModelSelection(selectedModel, 'AddModelInline');
|
|
||||||
|
|
||||||
// Reset form state
|
|
||||||
setSelectedProvider(null); // Clear the provider selection
|
|
||||||
setModelName(''); // Clear the model name
|
|
||||||
setFilteredModels([]);
|
|
||||||
setShowSuggestions(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSelectSuggestion = (suggestion: { provider: string; name: string }) => {
|
|
||||||
setModelName(suggestion.name);
|
|
||||||
setShowSuggestions(false); // Hide suggestions after selection
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleBlur = () => {
|
|
||||||
setTimeout(() => setShowSuggestions(false), 150); // Delay to allow click to register
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="mb-6">
|
|
||||||
<form className="grid grid-cols-[1.5fr_2fr_auto] gap-4 items-center">
|
|
||||||
<Select
|
|
||||||
options={providerOptions}
|
|
||||||
value={providerOptions.find((option) => option.value === selectedProvider) || null}
|
|
||||||
onChange={(newValue: unknown) => {
|
|
||||||
const option = newValue as { value: string | null } | null;
|
|
||||||
setSelectedProvider(option?.value || null);
|
|
||||||
setModelName(''); // Clear model name when provider changes
|
|
||||||
setFilteredModels([]);
|
|
||||||
}}
|
|
||||||
placeholder="Select provider"
|
|
||||||
isClearable
|
|
||||||
styles={createDarkSelectStyles('200px')}
|
|
||||||
theme={darkSelectTheme}
|
|
||||||
/>
|
|
||||||
<div className="relative" style={{ minWidth: '150px' }}>
|
|
||||||
<Input
|
|
||||||
type="text"
|
|
||||||
placeholder="Model name"
|
|
||||||
value={modelName}
|
|
||||||
onChange={(e) => setModelName(e.target.value)}
|
|
||||||
onBlur={handleBlur}
|
|
||||||
/>
|
|
||||||
{showSuggestions && (
|
|
||||||
<div className="absolute z-10 w-full mt-1 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-md shadow-lg">
|
|
||||||
{filteredModels.map((model) => (
|
|
||||||
<div
|
|
||||||
key={model.id}
|
|
||||||
className="p-2 cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-700 dark:text-white"
|
|
||||||
onClick={() => handleSelectSuggestion(model)}
|
|
||||||
>
|
|
||||||
{model.name}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
className="bg-black text-white hover:bg-black/90 dark:bg-white dark:text-black dark:hover:bg-white/90"
|
|
||||||
onClick={handleSubmit}
|
|
||||||
>
|
|
||||||
<Plus className="h-4 w-4" /> Add Model
|
|
||||||
</Button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
import { Model } from './ModelContext';
|
|
||||||
|
|
||||||
// TODO: move into backends / fetch dynamically
|
|
||||||
// this is used by ModelContext
|
|
||||||
export const gooseModels: Model[] = [
|
|
||||||
{ id: 1, name: 'gpt-4o-mini', provider: 'OpenAI' },
|
|
||||||
{ id: 2, name: 'gpt-4o', provider: 'OpenAI' },
|
|
||||||
{ id: 3, name: 'gpt-4-turbo', provider: 'OpenAI' },
|
|
||||||
{ id: 5, name: 'o1', provider: 'OpenAI' },
|
|
||||||
{ id: 7, name: 'claude-3-5-sonnet-latest', provider: 'Anthropic' },
|
|
||||||
{ id: 8, name: 'claude-3-5-haiku-latest', provider: 'Anthropic' },
|
|
||||||
{ id: 9, name: 'claude-3-opus-latest', provider: 'Anthropic' },
|
|
||||||
{ id: 10, name: 'gemini-1.5-pro', provider: 'Google' },
|
|
||||||
{ id: 11, name: 'gemini-1.5-flash', provider: 'Google' },
|
|
||||||
{ id: 12, name: 'gemini-2.0-flash', provider: 'Google' },
|
|
||||||
{ id: 13, name: 'gemini-2.0-flash-lite-preview-02-05', provider: 'Google' },
|
|
||||||
{ id: 14, name: 'gemini-2.0-flash-thinking-exp-01-21', provider: 'Google' },
|
|
||||||
{ id: 15, name: 'gemini-2.0-pro-exp-02-05', provider: 'Google' },
|
|
||||||
{ id: 16, name: 'gemini-2.5-pro-exp-03-25', provider: 'Google' },
|
|
||||||
{ id: 17, name: 'llama-3.3-70b-versatile', provider: 'Groq' },
|
|
||||||
{ id: 18, name: 'qwen2.5', provider: 'Ollama' },
|
|
||||||
{ id: 19, name: 'anthropic/claude-3.5-sonnet', provider: 'OpenRouter' },
|
|
||||||
{ id: 20, name: 'gpt-4o', provider: 'Azure OpenAI' },
|
|
||||||
{ id: 21, name: 'claude-3-7-sonnet@20250219', provider: 'GCP Vertex AI' },
|
|
||||||
{ id: 22, name: 'claude-3-5-sonnet-v2@20241022', provider: 'GCP Vertex AI' },
|
|
||||||
{ id: 23, name: 'claude-3-5-sonnet@20240620', provider: 'GCP Vertex AI' },
|
|
||||||
{ id: 24, name: 'claude-3-5-haiku@20241022', provider: 'GCP Vertex AI' },
|
|
||||||
{ id: 25, name: 'claude-sonnet-4@20250514', provider: 'GCP Vertex AI' },
|
|
||||||
{ id: 26, name: 'gemini-2.0-pro-exp-02-05', provider: 'GCP Vertex AI' },
|
|
||||||
{ id: 27, name: 'gemini-2.0-flash-001', provider: 'GCP Vertex AI' },
|
|
||||||
{ id: 28, name: 'gemini-1.5-pro-002', provider: 'GCP Vertex AI' },
|
|
||||||
{ id: 29, name: 'gemini-2.5-pro-exp-03-25', provider: 'GCP Vertex AI' },
|
|
||||||
];
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
import { createContext, useContext, useState, ReactNode } from 'react';
|
|
||||||
import { GOOSE_MODEL, GOOSE_PROVIDER } from '../../../env_vars';
|
|
||||||
import { gooseModels } from './GooseModels'; // Assuming hardcoded models are here
|
|
||||||
|
|
||||||
// TODO: API keys
|
|
||||||
export interface Model {
|
|
||||||
id?: number; // Make `id` optional to allow user-defined models
|
|
||||||
name: string;
|
|
||||||
provider: string;
|
|
||||||
lastUsed?: string;
|
|
||||||
alias?: string; // optional model display name
|
|
||||||
subtext?: string; // goes below model name if not the provider
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ModelContextValue {
|
|
||||||
currentModel: Model | null;
|
|
||||||
setCurrentModel: (model: Model) => void;
|
|
||||||
switchModel: (model: Model) => void; // Add the reusable switch function
|
|
||||||
}
|
|
||||||
|
|
||||||
const ModelContext = createContext<ModelContextValue | undefined>(undefined);
|
|
||||||
|
|
||||||
export const ModelProvider = ({ children }: { children: ReactNode }) => {
|
|
||||||
const [currentModel, setCurrentModel] = useState<Model | null>(
|
|
||||||
JSON.parse(localStorage.getItem(GOOSE_MODEL) || 'null')
|
|
||||||
);
|
|
||||||
|
|
||||||
const updateModel = (model: Model) => {
|
|
||||||
setCurrentModel(model);
|
|
||||||
localStorage.setItem(GOOSE_PROVIDER, model.provider.toLowerCase());
|
|
||||||
localStorage.setItem(GOOSE_MODEL, JSON.stringify(model));
|
|
||||||
};
|
|
||||||
|
|
||||||
const switchModel = (model: Model) => {
|
|
||||||
const newModel = model.id
|
|
||||||
? gooseModels.find((m) => m.id === model.id) || model
|
|
||||||
: { id: Date.now(), ...model }; // Assign unique ID for user-defined models
|
|
||||||
updateModel(newModel);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ModelContext.Provider value={{ currentModel, setCurrentModel: updateModel, switchModel }}>
|
|
||||||
{children}
|
|
||||||
</ModelContext.Provider>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const useModel = () => {
|
|
||||||
const context = useContext(ModelContext);
|
|
||||||
if (!context) throw new Error('useModel must be used within a ModelProvider');
|
|
||||||
return context;
|
|
||||||
};
|
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
|
||||||
import { useRecentModels } from './RecentModels';
|
|
||||||
import { useModel, Model } from './ModelContext';
|
|
||||||
import { useHandleModelSelection } from './utils';
|
|
||||||
import type { View } from '../../../App';
|
|
||||||
|
|
||||||
interface ModelRadioListProps {
|
|
||||||
renderItem: (props: {
|
|
||||||
model: Model;
|
|
||||||
isSelected: boolean;
|
|
||||||
onSelect: () => void;
|
|
||||||
}) => React.ReactNode;
|
|
||||||
className?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function SeeMoreModelsButtons({ setView }: { setView: (view: View) => void }) {
|
|
||||||
return (
|
|
||||||
<div className="flex justify-between items-center mb-6 border-b border-borderSubtle px-8">
|
|
||||||
<h2 className="text-xl font-medium text-textStandard">Models</h2>
|
|
||||||
<button
|
|
||||||
onClick={() => {
|
|
||||||
setView('moreModels');
|
|
||||||
}}
|
|
||||||
className="text-indigo-500 hover:text-indigo-600 text-sm"
|
|
||||||
>
|
|
||||||
Browse
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ModelRadioList({ renderItem, className = '' }: ModelRadioListProps) {
|
|
||||||
const { recentModels } = useRecentModels();
|
|
||||||
const { currentModel } = useModel();
|
|
||||||
const handleModelSelection = useHandleModelSelection();
|
|
||||||
const [selectedModel, setSelectedModel] = useState<string | null>(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (currentModel) {
|
|
||||||
setSelectedModel(currentModel.name);
|
|
||||||
}
|
|
||||||
}, [currentModel]);
|
|
||||||
|
|
||||||
const handleRadioChange = async (model: Model) => {
|
|
||||||
if (selectedModel === model.name) {
|
|
||||||
console.log(`Model "${model.name}" is already active.`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setSelectedModel(model.name);
|
|
||||||
await handleModelSelection(model, 'ModelList');
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={className}>
|
|
||||||
{recentModels.map((model) =>
|
|
||||||
renderItem({
|
|
||||||
model,
|
|
||||||
isSelected: selectedModel === model.name,
|
|
||||||
onSelect: () => handleRadioChange(model),
|
|
||||||
})
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,76 +0,0 @@
|
|||||||
import { RecentModels } from './RecentModels';
|
|
||||||
import { ProviderButtons } from './ProviderButtons';
|
|
||||||
import BackButton from '../../ui/BackButton';
|
|
||||||
import { SearchBar } from './Search';
|
|
||||||
import { AddModelInline } from './AddModelInline';
|
|
||||||
import { ScrollArea } from '../../ui/scroll-area';
|
|
||||||
import type { View } from '../../../App';
|
|
||||||
import MoreMenuLayout from '../../more_menu/MoreMenuLayout';
|
|
||||||
|
|
||||||
export default function MoreModelsView({
|
|
||||||
onClose,
|
|
||||||
setView,
|
|
||||||
}: {
|
|
||||||
onClose: () => void;
|
|
||||||
setView: (view: View) => void;
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<div className="h-screen w-full">
|
|
||||||
<MoreMenuLayout showMenu={false} />
|
|
||||||
|
|
||||||
<ScrollArea className="h-full w-full">
|
|
||||||
<div className="px-8 pt-6 pb-4">
|
|
||||||
<BackButton onClick={onClose} />
|
|
||||||
<h1 className="text-3xl font-medium text-textStandard mt-1">Browse models</h1>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Content Area */}
|
|
||||||
<div className="flex-1 py-8 pt-[20px]">
|
|
||||||
<div className="max-w-full md:max-w-3xl mx-auto space-y-12">
|
|
||||||
<div className="flex justify-between items-center mb-6 border-b border-borderSubtle px-8">
|
|
||||||
<h2 className="text-xl font-medium text-textStandard">Models</h2>
|
|
||||||
<button
|
|
||||||
onClick={() => setView('configureProviders')}
|
|
||||||
className="text-indigo-500 hover:text-indigo-600 text-sm"
|
|
||||||
>
|
|
||||||
Configure
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="px-8 space-y-8">
|
|
||||||
{/* Search Section */}
|
|
||||||
<section>
|
|
||||||
<h2 className="text-md font-medium text-textStandard mb-3">Search Models</h2>
|
|
||||||
<SearchBar />
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* Add Model Section */}
|
|
||||||
<section>
|
|
||||||
<h2 className="text-md font-medium text-textStandard mb-3">Add Model</h2>
|
|
||||||
<AddModelInline />
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* Provider Section */}
|
|
||||||
<section>
|
|
||||||
<h2 className="text-md font-medium text-textStandard mb-3">Browse by Provider</h2>
|
|
||||||
<div>
|
|
||||||
<ProviderButtons />
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* Recent Models Section */}
|
|
||||||
<section>
|
|
||||||
<div className="flex items-center justify-between mb-3">
|
|
||||||
<h2 className="text-md font-medium text-textStandard">Recently used</h2>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<RecentModels />
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</ScrollArea>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,100 +0,0 @@
|
|||||||
import { useState, useEffect } from 'react';
|
|
||||||
import { Button } from '../../ui/button';
|
|
||||||
import { Switch } from '../../ui/switch';
|
|
||||||
import { useActiveKeys } from '../api_keys/ActiveKeysContext';
|
|
||||||
import { model_docs_link } from './hardcoded_stuff';
|
|
||||||
import { gooseModels } from './GooseModels';
|
|
||||||
import { useModel } from './ModelContext';
|
|
||||||
import { useHandleModelSelection } from './utils';
|
|
||||||
|
|
||||||
// Create a mapping from provider name to href
|
|
||||||
const providerLinks: Record<string, string> = model_docs_link.reduce(
|
|
||||||
(acc, { name, href }) => {
|
|
||||||
acc[name] = href;
|
|
||||||
return acc;
|
|
||||||
},
|
|
||||||
{} as Record<string, string>
|
|
||||||
);
|
|
||||||
|
|
||||||
export function ProviderButtons() {
|
|
||||||
const { activeKeys } = useActiveKeys();
|
|
||||||
const [selectedProvider, setSelectedProvider] = useState<string | null>(null);
|
|
||||||
const { currentModel } = useModel();
|
|
||||||
const handleModelSelection = useHandleModelSelection();
|
|
||||||
|
|
||||||
// Handle Escape key press
|
|
||||||
useEffect(() => {
|
|
||||||
const handleEsc = (event: KeyboardEvent) => {
|
|
||||||
if (event.key === 'Escape') {
|
|
||||||
setSelectedProvider(null);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
window.addEventListener('keydown', handleEsc);
|
|
||||||
return () => window.removeEventListener('keydown', handleEsc);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// Filter models by provider
|
|
||||||
const providerModels = selectedProvider
|
|
||||||
? gooseModels.filter((model) => model.provider === selectedProvider)
|
|
||||||
: [];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="space-y-4">
|
|
||||||
<div className="overflow-x-auto">
|
|
||||||
<div className="flex items-center gap-2 min-w-min">
|
|
||||||
{activeKeys.map((provider) => (
|
|
||||||
<Button
|
|
||||||
key={provider}
|
|
||||||
variant="ghost"
|
|
||||||
className={`text-sm whitespace-nowrap shrink-0
|
|
||||||
${
|
|
||||||
selectedProvider === provider
|
|
||||||
? 'bg-bgSubtle text-textStandard border-borderStandard'
|
|
||||||
: 'bg-bgApp border-borderSubtle text-textSubtle'
|
|
||||||
}
|
|
||||||
rounded-full shadow-none border`}
|
|
||||||
onClick={() => {
|
|
||||||
setSelectedProvider(selectedProvider === provider ? null : provider);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{provider}
|
|
||||||
</Button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Models List */}
|
|
||||||
{selectedProvider && (
|
|
||||||
<div className="mt-6">
|
|
||||||
<div>
|
|
||||||
{providerModels.map((model) => (
|
|
||||||
<div
|
|
||||||
key={model.id}
|
|
||||||
className="py-2 px-1 cursor-pointer text-gray-600
|
|
||||||
dark:text-gray-300 hover:text-gray-900
|
|
||||||
dark:hover:text-white transition-colors
|
|
||||||
flex justify-between items-center"
|
|
||||||
>
|
|
||||||
<span>{model.name}</span>
|
|
||||||
<Switch
|
|
||||||
variant="mono"
|
|
||||||
checked={model.id === currentModel?.id}
|
|
||||||
onCheckedChange={() => handleModelSelection(model, 'ProviderButtons')}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<a
|
|
||||||
href={providerLinks[selectedProvider]}
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
className="text-indigo-500 hover:text-indigo-600 text-sm"
|
|
||||||
>
|
|
||||||
Browse more {selectedProvider} models
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,170 +0,0 @@
|
|||||||
import { useState, useEffect } from 'react';
|
|
||||||
import { Clock } from 'lucide-react';
|
|
||||||
import { Model } from './ModelContext';
|
|
||||||
import { ModelRadioList, SeeMoreModelsButtons } from './ModelRadioList';
|
|
||||||
import { useModel } from './ModelContext';
|
|
||||||
import { useHandleModelSelection } from './utils';
|
|
||||||
import type { View } from '../../../App';
|
|
||||||
|
|
||||||
const MAX_RECENT_MODELS = 3;
|
|
||||||
|
|
||||||
export function useRecentModels() {
|
|
||||||
const [recentModels, setRecentModels] = useState<Model[]>([]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const storedModels = localStorage.getItem('recentModels');
|
|
||||||
if (storedModels) {
|
|
||||||
setRecentModels(JSON.parse(storedModels));
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const addRecentModel = (model: Model) => {
|
|
||||||
const modelWithTimestamp = { ...model, lastUsed: new Date().toISOString() }; // Add lastUsed field
|
|
||||||
setRecentModels((prevModels) => {
|
|
||||||
const updatedModels = [
|
|
||||||
modelWithTimestamp,
|
|
||||||
...prevModels.filter((m) => m.name !== model.name),
|
|
||||||
].slice(0, MAX_RECENT_MODELS);
|
|
||||||
|
|
||||||
localStorage.setItem('recentModels', JSON.stringify(updatedModels));
|
|
||||||
return updatedModels;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return { recentModels, addRecentModel };
|
|
||||||
}
|
|
||||||
|
|
||||||
function getRelativeTimeString(date: string | Date): string {
|
|
||||||
const now = new Date();
|
|
||||||
const then = new Date(date);
|
|
||||||
const diffInSeconds = Math.floor((now.getTime() - then.getTime()) / 1000);
|
|
||||||
|
|
||||||
if (diffInSeconds < 60) {
|
|
||||||
return 'Just now';
|
|
||||||
}
|
|
||||||
|
|
||||||
const diffInMinutes = Math.floor(diffInSeconds / 60);
|
|
||||||
if (diffInMinutes < 60) {
|
|
||||||
return `${diffInMinutes}m ago`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const diffInHours = Math.floor(diffInMinutes / 60);
|
|
||||||
if (diffInHours < 24) {
|
|
||||||
return `${diffInHours}h ago`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const diffInDays = Math.floor(diffInHours / 24);
|
|
||||||
if (diffInDays < 7) {
|
|
||||||
return `${diffInDays}d ago`;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (diffInDays < 30) {
|
|
||||||
const weeks = Math.floor(diffInDays / 7);
|
|
||||||
return `${weeks}w ago`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const months = Math.floor(diffInDays / 30);
|
|
||||||
if (months < 12) {
|
|
||||||
return `${months}mo ago`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const years = Math.floor(months / 12);
|
|
||||||
return `${years}y ago`;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function RecentModels() {
|
|
||||||
const { recentModels } = useRecentModels();
|
|
||||||
const { currentModel } = useModel();
|
|
||||||
const handleModelSelection = useHandleModelSelection();
|
|
||||||
const [selectedModel, setSelectedModel] = useState<string | null>(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (currentModel) {
|
|
||||||
setSelectedModel(currentModel.name);
|
|
||||||
}
|
|
||||||
}, [currentModel]);
|
|
||||||
|
|
||||||
const handleRadioChange = async (model: Model) => {
|
|
||||||
if (selectedModel === model.name) {
|
|
||||||
console.log(`Model "${model.name}" is already active.`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setSelectedModel(model.name);
|
|
||||||
await handleModelSelection(model, 'RecentModels');
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="space-y-2">
|
|
||||||
{recentModels.map((model) => (
|
|
||||||
<label key={model.name} className="flex justify-between items-center py-2 cursor-pointer">
|
|
||||||
<div className="relative" onClick={() => handleRadioChange(model)}>
|
|
||||||
<div className="flex flex-row items-center">
|
|
||||||
<input
|
|
||||||
type="radio"
|
|
||||||
name="recentModels"
|
|
||||||
value={model.name}
|
|
||||||
checked={selectedModel === model.name}
|
|
||||||
onChange={() => handleRadioChange(model)}
|
|
||||||
className="peer sr-only"
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
className="h-4 w-4 rounded-full border border-gray-400 dark:border-gray-500 mr-4
|
|
||||||
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 className="">
|
|
||||||
<p className="text-sm text-textStandard">{model.name}</p>
|
|
||||||
<p className="text-xs text-textSubtle">{model.provider}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center text-sm text-textSubtle">
|
|
||||||
<Clock className="w-4 h-4 mr-2" />
|
|
||||||
{model.lastUsed ? getRelativeTimeString(model.lastUsed) : 'N/A'}
|
|
||||||
</div>
|
|
||||||
</label>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function RecentModelsRadio({ setView }: { setView: (view: View) => void }) {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<SeeMoreModelsButtons setView={setView} />
|
|
||||||
<div className="px-8">
|
|
||||||
<div className="space-y-2">
|
|
||||||
<ModelRadioList
|
|
||||||
renderItem={({ model, isSelected, onSelect }) => (
|
|
||||||
<label key={model.name} className="flex items-center py-2 cursor-pointer">
|
|
||||||
<div className="relative mr-4">
|
|
||||||
<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 className="">
|
|
||||||
<p className="text-sm text-textStandard">{model.alias ?? model.name}</p>
|
|
||||||
<p className="text-xs text-textSubtle">{model.subtext ?? model.provider}</p>
|
|
||||||
</div>
|
|
||||||
</label>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,121 +0,0 @@
|
|||||||
import React, { useState, useEffect, useRef } from 'react';
|
|
||||||
import { Search } from 'lucide-react';
|
|
||||||
import { Switch } from '../../ui/switch';
|
|
||||||
import { gooseModels } from './GooseModels';
|
|
||||||
import { useModel } from './ModelContext';
|
|
||||||
import { useHandleModelSelection } from './utils';
|
|
||||||
import { useActiveKeys } from '../api_keys/ActiveKeysContext';
|
|
||||||
|
|
||||||
// TODO: dark mode (p1)
|
|
||||||
// FIXME: arrow keys do not work to select a model (p1)
|
|
||||||
export function SearchBar() {
|
|
||||||
const [search, setSearch] = useState('');
|
|
||||||
const [focusedIndex, setFocusedIndex] = useState(-1);
|
|
||||||
const [showResults, setShowResults] = useState(false);
|
|
||||||
const resultsRef = useRef<(HTMLDivElement | null)[]>([]);
|
|
||||||
const searchBarRef = useRef<HTMLDivElement>(null);
|
|
||||||
|
|
||||||
const { currentModel } = useModel(); // Access global state
|
|
||||||
const handleModelSelection = useHandleModelSelection();
|
|
||||||
|
|
||||||
// search results filtering
|
|
||||||
// results set will only include models that have a configured provider
|
|
||||||
const { activeKeys } = useActiveKeys(); // Access active keys from context
|
|
||||||
|
|
||||||
const model_options = gooseModels.filter((model) => activeKeys.includes(model.provider));
|
|
||||||
|
|
||||||
const filteredModels = model_options
|
|
||||||
.filter((model) => model.name.toLowerCase().includes(search.toLowerCase()))
|
|
||||||
.slice(0, 5);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setFocusedIndex(-1);
|
|
||||||
}, [search]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const handleClickOutside = (event: MouseEvent) => {
|
|
||||||
if (searchBarRef.current && !searchBarRef.current.contains(event.target as Node)) {
|
|
||||||
setShowResults(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
document.addEventListener('mousedown', handleClickOutside);
|
|
||||||
return () => {
|
|
||||||
document.removeEventListener('mousedown', handleClickOutside);
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
|
||||||
if (e.key === 'ArrowDown') {
|
|
||||||
e.preventDefault();
|
|
||||||
setFocusedIndex((prev) => (prev < filteredModels.length - 1 ? prev + 1 : prev));
|
|
||||||
setShowResults(true);
|
|
||||||
} else if (e.key === 'ArrowUp') {
|
|
||||||
e.preventDefault();
|
|
||||||
setFocusedIndex((prev) => (prev > 0 ? prev - 1 : prev));
|
|
||||||
setShowResults(true);
|
|
||||||
} else if (e.key === 'Enter' && focusedIndex >= 0) {
|
|
||||||
e.preventDefault();
|
|
||||||
const selectedModel = filteredModels[focusedIndex];
|
|
||||||
handleModelSelection(selectedModel, 'SearchBar');
|
|
||||||
} else if (e.key === 'Escape') {
|
|
||||||
e.preventDefault();
|
|
||||||
setShowResults(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (focusedIndex >= 0 && focusedIndex < resultsRef.current.length) {
|
|
||||||
resultsRef.current[focusedIndex]?.scrollIntoView({
|
|
||||||
block: 'nearest',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, [focusedIndex]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="relative" ref={searchBarRef}>
|
|
||||||
<Search className="absolute left-3 top-[0.8rem] h-4 w-4 text-textSubtle" />
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
placeholder="Search models..."
|
|
||||||
value={search}
|
|
||||||
onChange={(e) => {
|
|
||||||
setSearch(e.target.value);
|
|
||||||
setShowResults(true);
|
|
||||||
}}
|
|
||||||
onKeyDown={handleKeyDown}
|
|
||||||
onFocus={() => setShowResults(true)}
|
|
||||||
className="w-full pl-9 py-2 text-black dark:text-white bg-bgApp border border-borderSubtle rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
||||||
/>
|
|
||||||
{showResults && search && (
|
|
||||||
<div className="absolute z-10 w-full mt-2 bg-white dark:bg-gray-800 border border-borderSubtle rounded-md shadow-lg">
|
|
||||||
{filteredModels.length > 0 ? (
|
|
||||||
filteredModels.map((model, index) => (
|
|
||||||
<div
|
|
||||||
key={model.id}
|
|
||||||
ref={(el) => (resultsRef.current[index] = el)}
|
|
||||||
className={`p-2 flex justify-between items-center hover:bg-bgSubtle/50 dark:hover:bg-gray-700 cursor-pointer ${
|
|
||||||
model.id === currentModel?.id ? 'bg-bgSubtle/50 dark:bg-gray-700' : ''
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
<span className="font-medium dark:text-white">{model.name}</span>
|
|
||||||
<span className="ml-2 text-xs text-gray-500 dark:text-gray-400 italic">
|
|
||||||
{model.provider}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<Switch
|
|
||||||
variant="mono"
|
|
||||||
checked={model.id === currentModel?.id}
|
|
||||||
onCheckedChange={() => handleModelSelection(model, 'SearchBar')}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
))
|
|
||||||
) : (
|
|
||||||
<div className="p-2 text-textSubtle dark:text-gray-400">No models found</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
import { Sliders } from 'lucide-react';
|
import { Sliders } from 'lucide-react';
|
||||||
import React, { useEffect, useState, useRef } from 'react';
|
import React, { useEffect, useState, useRef } from 'react';
|
||||||
import { useConfig } from '../../../ConfigContext';
|
import { useModelAndProvider } from '../../../ModelAndProviderContext';
|
||||||
import { getCurrentModelAndProviderForDisplay } from '../index';
|
|
||||||
import { AddModelModal } from '../subcomponents/AddModelModal';
|
import { AddModelModal } from '../subcomponents/AddModelModal';
|
||||||
import { View } from '../../../../App';
|
import { View } from '../../../../App';
|
||||||
import { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } from '../../../ui/Tooltip';
|
import { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } from '../../../ui/Tooltip';
|
||||||
@@ -11,10 +10,10 @@ interface ModelsBottomBarProps {
|
|||||||
setView: (view: View) => void;
|
setView: (view: View) => void;
|
||||||
}
|
}
|
||||||
export default function ModelsBottomBar({ dropdownRef, setView }: ModelsBottomBarProps) {
|
export default function ModelsBottomBar({ dropdownRef, setView }: ModelsBottomBarProps) {
|
||||||
const { read, getProviders } = useConfig();
|
const { currentModel, currentProvider, getCurrentModelAndProviderForDisplay } =
|
||||||
|
useModelAndProvider();
|
||||||
const [isModelMenuOpen, setIsModelMenuOpen] = useState(false);
|
const [isModelMenuOpen, setIsModelMenuOpen] = useState(false);
|
||||||
const [provider, setProvider] = useState<string | null>(null);
|
const [displayProvider, setDisplayProvider] = useState<string | null>(null);
|
||||||
const [model, setModel] = useState<string>('');
|
|
||||||
const [isAddModelModalOpen, setIsAddModelModalOpen] = useState(false);
|
const [isAddModelModalOpen, setIsAddModelModalOpen] = useState(false);
|
||||||
const menuRef = useRef<HTMLDivElement>(null);
|
const menuRef = useRef<HTMLDivElement>(null);
|
||||||
const [isModelTruncated, setIsModelTruncated] = useState(false);
|
const [isModelTruncated, setIsModelTruncated] = useState(false);
|
||||||
@@ -22,16 +21,15 @@ export default function ModelsBottomBar({ dropdownRef, setView }: ModelsBottomBa
|
|||||||
const modelRef = useRef<HTMLSpanElement>(null);
|
const modelRef = useRef<HTMLSpanElement>(null);
|
||||||
const [isTooltipOpen, setIsTooltipOpen] = useState(false);
|
const [isTooltipOpen, setIsTooltipOpen] = useState(false);
|
||||||
|
|
||||||
|
// Update display provider when current provider changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
(async () => {
|
if (currentProvider) {
|
||||||
const modelProvider = await getCurrentModelAndProviderForDisplay({
|
(async () => {
|
||||||
readFromConfig: read,
|
const modelProvider = await getCurrentModelAndProviderForDisplay();
|
||||||
getProviders,
|
setDisplayProvider(modelProvider.provider);
|
||||||
});
|
})();
|
||||||
setProvider(modelProvider.provider as string | null);
|
}
|
||||||
setModel(modelProvider.model as string);
|
}, [currentProvider, getCurrentModelAndProviderForDisplay]);
|
||||||
})();
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const checkTruncation = () => {
|
const checkTruncation = () => {
|
||||||
@@ -42,7 +40,7 @@ export default function ModelsBottomBar({ dropdownRef, setView }: ModelsBottomBa
|
|||||||
checkTruncation();
|
checkTruncation();
|
||||||
window.addEventListener('resize', checkTruncation);
|
window.addEventListener('resize', checkTruncation);
|
||||||
return () => window.removeEventListener('resize', checkTruncation);
|
return () => window.removeEventListener('resize', checkTruncation);
|
||||||
}, [model]);
|
}, [currentModel]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setIsTooltipOpen(false);
|
setIsTooltipOpen(false);
|
||||||
@@ -81,12 +79,12 @@ export default function ModelsBottomBar({ dropdownRef, setView }: ModelsBottomBa
|
|||||||
ref={modelRef}
|
ref={modelRef}
|
||||||
className="truncate max-w-[130px] md:max-w-[200px] lg:max-w-[360px] min-w-0 block"
|
className="truncate max-w-[130px] md:max-w-[200px] lg:max-w-[360px] min-w-0 block"
|
||||||
>
|
>
|
||||||
{model || 'Select Model'}
|
{currentModel || 'Select Model'}
|
||||||
</span>
|
</span>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
{isModelTruncated && (
|
{isModelTruncated && (
|
||||||
<TooltipContent className="max-w-96 overflow-auto scrollbar-thin" side="top">
|
<TooltipContent className="max-w-96 overflow-auto scrollbar-thin" side="top">
|
||||||
{model || 'Select Model'}
|
{currentModel || 'Select Model'}
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
)}
|
)}
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
@@ -99,7 +97,7 @@ export default function ModelsBottomBar({ dropdownRef, setView }: ModelsBottomBa
|
|||||||
<div className="">
|
<div className="">
|
||||||
<div className="text-sm text-textProminent mt-2 ml-2">Current:</div>
|
<div className="text-sm text-textProminent mt-2 ml-2">Current:</div>
|
||||||
<div className="flex items-center justify-between text-sm ml-2">
|
<div className="flex items-center justify-between text-sm ml-2">
|
||||||
{model} -- {provider}
|
{currentModel} -- {displayProvider}
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className="flex items-center justify-between text-textStandard p-2 cursor-pointer transition-colors hover:bg-bgStandard
|
className="flex items-center justify-between text-textStandard p-2 cursor-pointer transition-colors hover:bg-bgStandard
|
||||||
@@ -1,93 +0,0 @@
|
|||||||
export const default_models = {
|
|
||||||
openai: 'gpt-4o',
|
|
||||||
anthropic: 'claude-3-5-sonnet-latest',
|
|
||||||
databricks: 'goose',
|
|
||||||
google: 'gemini-2.0-flash-exp',
|
|
||||||
groq: 'llama-3.3-70b-versatile',
|
|
||||||
openrouter: 'anthropic/claude-3.5-sonnet',
|
|
||||||
ollama: 'qwen2.5',
|
|
||||||
azure_openai: 'gpt-4o',
|
|
||||||
gcp_vertex_ai: 'gemini-2.0-flash-001',
|
|
||||||
aws_bedrock: 'us.anthropic.claude-3-7-sonnet-20250219-v1:0',
|
|
||||||
snowflake: 'claude-3-5-sonnet',
|
|
||||||
venice: 'llama-3.3-70b',
|
|
||||||
};
|
|
||||||
|
|
||||||
export function getDefaultModel(key: string): string | undefined {
|
|
||||||
return default_models[key as keyof typeof default_models] || undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const required_keys = {
|
|
||||||
OpenAI: ['OPENAI_API_KEY', 'OPENAI_HOST', 'OPENAI_BASE_PATH'],
|
|
||||||
Anthropic: ['ANTHROPIC_API_KEY', 'ANTHROPIC_HOST'],
|
|
||||||
Databricks: ['DATABRICKS_HOST'],
|
|
||||||
Groq: ['GROQ_API_KEY'],
|
|
||||||
Ollama: ['OLLAMA_HOST'],
|
|
||||||
Google: ['GOOGLE_API_KEY'],
|
|
||||||
OpenRouter: ['OPENROUTER_API_KEY'],
|
|
||||||
Snowflake: ['SNOWFLAKE_HOST', 'SNOWFLAKE_TOKEN'],
|
|
||||||
'Azure OpenAI': [
|
|
||||||
'AZURE_OPENAI_ENDPOINT',
|
|
||||||
'AZURE_OPENAI_DEPLOYMENT_NAME',
|
|
||||||
'AZURE_OPENAI_API_VERSION',
|
|
||||||
],
|
|
||||||
'GCP Vertex AI': ['GCP_PROJECT_ID', 'GCP_LOCATION'],
|
|
||||||
'AWS Bedrock': ['AWS_PROFILE'],
|
|
||||||
};
|
|
||||||
|
|
||||||
export const default_key_value = {
|
|
||||||
ANTHROPIC_HOST: 'https://api.anthropic.com',
|
|
||||||
OPENAI_HOST: 'https://api.openai.com',
|
|
||||||
OPENAI_BASE_PATH: 'v1/chat/completions',
|
|
||||||
OLLAMA_HOST: 'localhost',
|
|
||||||
GCP_LOCATION: 'us-central1',
|
|
||||||
AZURE_OPENAI_API_VERSION: '2024-10-21',
|
|
||||||
};
|
|
||||||
|
|
||||||
export const supported_providers = [
|
|
||||||
'OpenAI',
|
|
||||||
'Anthropic',
|
|
||||||
'Databricks',
|
|
||||||
'Groq',
|
|
||||||
'Google',
|
|
||||||
'Ollama',
|
|
||||||
'OpenRouter',
|
|
||||||
'Azure OpenAI',
|
|
||||||
'GCP Vertex AI',
|
|
||||||
'AWS Bedrock',
|
|
||||||
'Snowflake',
|
|
||||||
];
|
|
||||||
|
|
||||||
export const model_docs_link = [
|
|
||||||
{ name: 'OpenAI', href: 'https://platform.openai.com/docs/models' },
|
|
||||||
{ name: 'Anthropic', href: 'https://docs.anthropic.com/en/docs/about-claude/models' },
|
|
||||||
{ name: 'Google', href: 'https://ai.google/get-started/our-models/' },
|
|
||||||
{ name: 'Groq', href: 'https://console.groq.com/docs/models' },
|
|
||||||
{
|
|
||||||
name: 'Databricks',
|
|
||||||
href: 'https://docs.databricks.com/en/generative-ai/external-models/index.html',
|
|
||||||
},
|
|
||||||
{ name: 'OpenRouter', href: 'https://openrouter.ai/models' },
|
|
||||||
{ name: 'Ollama', href: 'https://ollama.com/library' },
|
|
||||||
{ name: 'GCP Vertex AI', href: 'https://cloud.google.com/vertex-ai' },
|
|
||||||
{ name: 'AWS Bedrock', href: 'https://console.aws.amazon.com/bedrock/home#/model-catalog' },
|
|
||||||
{
|
|
||||||
name: 'Snowflake',
|
|
||||||
href: 'https://docs.snowflake.com/user-guide/snowflake-cortex/llm-functions#availability',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
export const provider_aliases = [
|
|
||||||
{ provider: 'OpenAI', alias: 'openai' },
|
|
||||||
{ provider: 'Anthropic', alias: 'anthropic' },
|
|
||||||
{ provider: 'Ollama', alias: 'ollama' },
|
|
||||||
{ provider: 'Groq', alias: 'groq' },
|
|
||||||
{ provider: 'Databricks', alias: 'databricks' },
|
|
||||||
{ provider: 'OpenRouter', alias: 'openrouter' },
|
|
||||||
{ provider: 'Google', alias: 'google' },
|
|
||||||
{ provider: 'Azure OpenAI', alias: 'azure_openai' },
|
|
||||||
{ provider: 'GCP Vertex AI', alias: 'gcp_vertex_ai' },
|
|
||||||
{ provider: 'AWS Bedrock', alias: 'aws_bedrock' },
|
|
||||||
{ provider: 'Snowflake', alias: 'snowflake' },
|
|
||||||
{ provider: 'Venice', alias: 'venice' },
|
|
||||||
];
|
|
||||||
@@ -1,8 +1,7 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import Model from '../modelInterface';
|
import Model from '../modelInterface';
|
||||||
import { useRecentModels } from './recentModels';
|
import { useRecentModels } from './recentModels';
|
||||||
import { changeModel, getCurrentModelAndProvider } from '../index';
|
import { useModelAndProvider } from '../../../ModelAndProviderContext';
|
||||||
import { useConfig } from '../../../ConfigContext';
|
|
||||||
import { toastInfo } from '../../../../toasts';
|
import { toastInfo } from '../../../../toasts';
|
||||||
|
|
||||||
interface ModelRadioListProps {
|
interface ModelRadioListProps {
|
||||||
@@ -30,7 +29,7 @@ export function BaseModelsList({
|
|||||||
} else {
|
} else {
|
||||||
modelList = providedModelList;
|
modelList = providedModelList;
|
||||||
}
|
}
|
||||||
const { read, upsert } = useConfig();
|
const { changeModel, getCurrentModelAndProvider } = useModelAndProvider();
|
||||||
const [selectedModel, setSelectedModel] = useState<Model | null>(null);
|
const [selectedModel, setSelectedModel] = useState<Model | null>(null);
|
||||||
const [isInitialized, setIsInitialized] = useState(false);
|
const [isInitialized, setIsInitialized] = useState(false);
|
||||||
|
|
||||||
@@ -40,10 +39,7 @@ export function BaseModelsList({
|
|||||||
|
|
||||||
const initializeCurrentModel = async () => {
|
const initializeCurrentModel = async () => {
|
||||||
try {
|
try {
|
||||||
const result = await getCurrentModelAndProvider({
|
const result = await getCurrentModelAndProvider();
|
||||||
readFromConfig: read,
|
|
||||||
writeToConfig: upsert,
|
|
||||||
});
|
|
||||||
if (isMounted) {
|
if (isMounted) {
|
||||||
// try to look up the model in the modelList
|
// try to look up the model in the modelList
|
||||||
let currentModel: Model;
|
let currentModel: Model;
|
||||||
@@ -72,10 +68,10 @@ export function BaseModelsList({
|
|||||||
return () => {
|
return () => {
|
||||||
isMounted = false;
|
isMounted = false;
|
||||||
};
|
};
|
||||||
}, [read, modelList, upsert]);
|
}, [getCurrentModelAndProvider, modelList]);
|
||||||
|
|
||||||
const handleModelSelection = async (model: Model) => {
|
const handleModelSelection = async (model: Model) => {
|
||||||
await changeModel({ model: model, writeToConfig: upsert });
|
await changeModel(model);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleRadioChange = async (model: Model) => {
|
const handleRadioChange = async (model: Model) => {
|
||||||
@@ -104,10 +100,7 @@ export function BaseModelsList({
|
|||||||
// If the operation fails, revert to the previous state by simply
|
// If the operation fails, revert to the previous state by simply
|
||||||
// re-calling the getCurrentModelAndProvider function
|
// re-calling the getCurrentModelAndProvider function
|
||||||
try {
|
try {
|
||||||
const result = await getCurrentModelAndProvider({
|
const result = await getCurrentModelAndProvider();
|
||||||
readFromConfig: read,
|
|
||||||
writeToConfig: upsert,
|
|
||||||
});
|
|
||||||
|
|
||||||
const currentModel =
|
const currentModel =
|
||||||
modelList.find((m) => m.name === result.model && m.provider === result.provider) ||
|
modelList.find((m) => m.name === result.model && m.provider === result.provider) ||
|
||||||
@@ -7,10 +7,9 @@ import { QUICKSTART_GUIDE_URL } from '../../providers/modal/constants';
|
|||||||
import { Input } from '../../../ui/input';
|
import { Input } from '../../../ui/input';
|
||||||
import { Select } from '../../../ui/Select';
|
import { Select } from '../../../ui/Select';
|
||||||
import { useConfig } from '../../../ConfigContext';
|
import { useConfig } from '../../../ConfigContext';
|
||||||
import { changeModel } from '../index';
|
import { useModelAndProvider } from '../../../ModelAndProviderContext';
|
||||||
import type { View } from '../../../../App';
|
import type { View } from '../../../../App';
|
||||||
import Model, { getProviderMetadata } from '../modelInterface';
|
import Model, { getProviderMetadata } from '../modelInterface';
|
||||||
import { useModel } from '../../../settings/models/ModelContext';
|
|
||||||
|
|
||||||
const ModalButtons = ({
|
const ModalButtons = ({
|
||||||
onSubmit,
|
onSubmit,
|
||||||
@@ -48,8 +47,8 @@ type AddModelModalProps = {
|
|||||||
setView: (view: View) => void;
|
setView: (view: View) => void;
|
||||||
};
|
};
|
||||||
export const AddModelModal = ({ onClose, setView }: AddModelModalProps) => {
|
export const AddModelModal = ({ onClose, setView }: AddModelModalProps) => {
|
||||||
const { getProviders, upsert } = useConfig();
|
const { getProviders } = useConfig();
|
||||||
const { switchModel } = useModel();
|
const { changeModel } = useModelAndProvider();
|
||||||
const [providerOptions, setProviderOptions] = useState<{ value: string; label: string }[]>([]);
|
const [providerOptions, setProviderOptions] = useState<{ value: string; label: string }[]>([]);
|
||||||
const [modelOptions, setModelOptions] = useState<
|
const [modelOptions, setModelOptions] = useState<
|
||||||
{ options: { value: string; label: string; provider: string }[] }[]
|
{ options: { value: string; label: string; provider: string }[] }[]
|
||||||
@@ -97,13 +96,7 @@ export const AddModelModal = ({ onClose, setView }: AddModelModalProps) => {
|
|||||||
|
|
||||||
const modelObj = { name: model, provider: provider, subtext: providerDisplayName } as Model;
|
const modelObj = { name: model, provider: provider, subtext: providerDisplayName } as Model;
|
||||||
|
|
||||||
await changeModel({
|
await changeModel(modelObj);
|
||||||
model: modelObj,
|
|
||||||
writeToConfig: upsert,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Update the model context
|
|
||||||
switchModel(modelObj);
|
|
||||||
|
|
||||||
onClose();
|
onClose();
|
||||||
}
|
}
|
||||||
@@ -1,87 +0,0 @@
|
|||||||
import { useModel } from './ModelContext'; // Import the useModel hook
|
|
||||||
import { Model } from './ModelContext';
|
|
||||||
import { useMemo } from 'react';
|
|
||||||
import { gooseModels } from './GooseModels';
|
|
||||||
import { toastError, toastSuccess } from '../../../toasts';
|
|
||||||
import { initializeSystem } from '../../../utils/providerUtils';
|
|
||||||
import { useRecentModels } from './RecentModels';
|
|
||||||
|
|
||||||
export function useHandleModelSelection() {
|
|
||||||
const { switchModel, currentModel } = useModel(); // Access switchModel via useModel
|
|
||||||
const { addRecentModel } = useRecentModels(); // Access addRecentModel from useRecentModels
|
|
||||||
|
|
||||||
return async (model: Model, componentName?: string) => {
|
|
||||||
try {
|
|
||||||
// Check if the selected model is already the active model
|
|
||||||
if (currentModel && currentModel.id === model.id) {
|
|
||||||
console.log(`[${componentName}] Selected model is already active: ${model.name}`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Call the context's switchModel to update the model
|
|
||||||
switchModel(model);
|
|
||||||
|
|
||||||
// Keep track of the recently used models
|
|
||||||
addRecentModel(model);
|
|
||||||
|
|
||||||
// switch models via the backend
|
|
||||||
// initialize agent
|
|
||||||
await initializeSystem(model.provider.toLowerCase(), model.name);
|
|
||||||
|
|
||||||
// Log to the console for tracking
|
|
||||||
console.log(`[${componentName}] Switched to model: ${model.name} (${model.provider})`);
|
|
||||||
|
|
||||||
// Display a success toast notification
|
|
||||||
toastSuccess({
|
|
||||||
title: 'Model changed',
|
|
||||||
msg: `Switched to ${model.alias ?? model.name}`,
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
// Handle errors gracefully
|
|
||||||
console.error(`[${componentName}] Failed to switch model:`, error);
|
|
||||||
// Display an error toast notification
|
|
||||||
toastError({
|
|
||||||
title: model.name,
|
|
||||||
msg: `Failed to switch to model`,
|
|
||||||
traceback: error instanceof Error ? error.message : String(error),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createSelectedModel(selectedProvider: string, modelName: string) {
|
|
||||||
let selectedModel = gooseModels.find(
|
|
||||||
(model) =>
|
|
||||||
model.provider.toLowerCase() === selectedProvider &&
|
|
||||||
model.name.toLowerCase() === modelName.toLowerCase()
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!selectedModel) {
|
|
||||||
// Normalize the casing for the provider using the first matching model
|
|
||||||
const normalizedProvider =
|
|
||||||
gooseModels.find((model) => model.provider.toLowerCase() === selectedProvider)?.provider ||
|
|
||||||
selectedProvider;
|
|
||||||
|
|
||||||
// Construct a model object
|
|
||||||
selectedModel = {
|
|
||||||
name: modelName,
|
|
||||||
provider: normalizedProvider, // Use normalized provider
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return selectedModel;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useFilteredModels(search: string, activeKeys: string[]) {
|
|
||||||
const filteredModels = useMemo(() => {
|
|
||||||
const modelOptions = gooseModels.filter((model) => activeKeys.includes(model.provider));
|
|
||||||
|
|
||||||
if (!search) {
|
|
||||||
return modelOptions; // Return all models if no search term
|
|
||||||
}
|
|
||||||
|
|
||||||
return modelOptions.filter((model) => model.name.toLowerCase().includes(search.toLowerCase()));
|
|
||||||
}, [search, activeKeys]);
|
|
||||||
|
|
||||||
return filteredModels;
|
|
||||||
}
|
|
||||||
@@ -1,281 +0,0 @@
|
|||||||
import { Check, Plus, Settings, X, Rocket } from 'lucide-react';
|
|
||||||
import { Button } from '../../ui/button';
|
|
||||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '../../ui/Tooltip';
|
|
||||||
import { Portal } from '@radix-ui/react-portal';
|
|
||||||
import { required_keys } from '../models/hardcoded_stuff';
|
|
||||||
|
|
||||||
// Common interfaces and helper functions
|
|
||||||
interface Provider {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
isConfigured: boolean;
|
|
||||||
description: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface BaseProviderCardProps {
|
|
||||||
name: string;
|
|
||||||
description: string;
|
|
||||||
isConfigured: boolean;
|
|
||||||
isSelected?: boolean;
|
|
||||||
isSelectable?: boolean;
|
|
||||||
onSelect?: () => void;
|
|
||||||
onAddKeys?: () => void;
|
|
||||||
onConfigure?: () => void;
|
|
||||||
showSettings?: boolean;
|
|
||||||
onDelete?: () => void;
|
|
||||||
showDelete?: boolean;
|
|
||||||
hasRequiredKeys?: boolean;
|
|
||||||
onTakeoff?: () => void;
|
|
||||||
showTakeoff?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getArticle(word: string): string {
|
|
||||||
return 'aeiouAEIOU'.indexOf(word[0]) >= 0 ? 'an' : 'a';
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getProviderDescription(provider: string) {
|
|
||||||
const descriptions = {
|
|
||||||
OpenAI: 'Access GPT-4 and other OpenAI models, including OpenAI compatible ones',
|
|
||||||
Anthropic: 'Access Claude and other Anthropic models',
|
|
||||||
Google: 'Access Gemini and other Google AI models',
|
|
||||||
Groq: 'Access Mixtral and other Groq-hosted models',
|
|
||||||
Databricks: 'Access models hosted on your Databricks instance',
|
|
||||||
OpenRouter: 'Access a variety of AI models through OpenRouter',
|
|
||||||
Ollama: 'Run and use open-source models locally',
|
|
||||||
};
|
|
||||||
return descriptions[provider as keyof typeof descriptions] || `Access ${provider} models`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function BaseProviderCard({
|
|
||||||
name,
|
|
||||||
description,
|
|
||||||
isConfigured,
|
|
||||||
isSelected,
|
|
||||||
isSelectable,
|
|
||||||
onSelect,
|
|
||||||
onAddKeys,
|
|
||||||
onConfigure,
|
|
||||||
showSettings,
|
|
||||||
onDelete,
|
|
||||||
showDelete = false,
|
|
||||||
hasRequiredKeys = false,
|
|
||||||
onTakeoff,
|
|
||||||
showTakeoff,
|
|
||||||
}: BaseProviderCardProps) {
|
|
||||||
const numRequiredKeys = (required_keys as Record<string, string[]>)[name]?.length || 0;
|
|
||||||
const tooltipText = numRequiredKeys === 1 ? `Add ${name} API Key` : `Add ${name} API Keys`;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className="relative h-full p-[2px] overflow-hidden rounded-[9px] group/card bg-borderSubtle hover:bg-transparent hover:duration-300"
|
|
||||||
data-testid={`provider-card-${name.toLowerCase()}`}
|
|
||||||
>
|
|
||||||
{/* Glowing ring */}
|
|
||||||
<div
|
|
||||||
className={`absolute pointer-events-none w-[260px] h-[260px] top-[-50px] left-[-30px] origin-center bg-[linear-gradient(45deg,#13BBAF,#FF4F00)] animate-[rotate_6s_linear_infinite] z-[-1] ${
|
|
||||||
isSelected ? 'opacity-100' : 'opacity-0 group-hover/card:opacity-100'
|
|
||||||
}`}
|
|
||||||
></div>
|
|
||||||
<div
|
|
||||||
onClick={() => isSelectable && isConfigured && onSelect?.()}
|
|
||||||
className={`relative bg-bgApp rounded-lg
|
|
||||||
p-3 transition-all duration-200 h-[160px] flex flex-col justify-between
|
|
||||||
${isSelectable && isConfigured ? 'cursor-pointer' : ''}
|
|
||||||
${!isSelectable ? 'hover:border-borderStandard' : ''}
|
|
||||||
${isSelectable && isConfigured ? 'hover:border-borderStandard' : ''}
|
|
||||||
`}
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
<div className="flex items-center">
|
|
||||||
<h3 className="text-base font-medium text-textStandard truncate mr-2">{name}</h3>
|
|
||||||
|
|
||||||
{/* Configured state: Green check */}
|
|
||||||
{isConfigured && (
|
|
||||||
<TooltipProvider>
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<div className="flex items-center justify-center w-5 h-5 rounded-full bg-green-100 dark:bg-green-900/30 shrink-0">
|
|
||||||
<Check className="h-3 w-3 text-green-600 dark:text-green-500" />
|
|
||||||
</div>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<Portal>
|
|
||||||
<TooltipContent side="top" align="center" className="z-[9999]">
|
|
||||||
<p>
|
|
||||||
{hasRequiredKeys
|
|
||||||
? `You have ${getArticle(name)} ${name} API Key set in your environment`
|
|
||||||
: `${name} is installed and running on your machine`}
|
|
||||||
</p>
|
|
||||||
</TooltipContent>
|
|
||||||
</Portal>
|
|
||||||
</Tooltip>
|
|
||||||
</TooltipProvider>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<p className="text-xs text-textSubtle mt-1.5 mb-3 leading-normal scrollbar-thin overflow-y-auto max-h-[54px] ">
|
|
||||||
{description}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-x-2 text-center flex items-center justify-between">
|
|
||||||
<div className="space-x-2">
|
|
||||||
{/* Default "Add Keys" Button for other providers */}
|
|
||||||
{!isConfigured && onAddKeys && hasRequiredKeys && (
|
|
||||||
<TooltipProvider>
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<Button
|
|
||||||
variant="default"
|
|
||||||
size="sm"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
onAddKeys();
|
|
||||||
}}
|
|
||||||
className="rounded-full h-7 w-7 p-0 bg-bgApp hover:bg-bgApp shadow-none text-textSubtle border border-borderSubtle hover:border-borderStandard hover:text-textStandard transition-colors"
|
|
||||||
>
|
|
||||||
<Plus className="!size-4" />
|
|
||||||
</Button>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<Portal>
|
|
||||||
<TooltipContent side="top" align="center" className="z-[9999]">
|
|
||||||
<p>{tooltipText}</p>
|
|
||||||
</TooltipContent>
|
|
||||||
</Portal>
|
|
||||||
</Tooltip>
|
|
||||||
</TooltipProvider>
|
|
||||||
)}
|
|
||||||
{isConfigured && showSettings && hasRequiredKeys && (
|
|
||||||
<TooltipProvider>
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
className="rounded-full h-7 w-7 p-0 bg-bgApp hover:bg-bgApp shadow-none text-textSubtle border border-borderSubtle hover:border-borderStandard hover:text-textStandard transition-colors"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
onConfigure?.();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Settings className="!size-4" />
|
|
||||||
</Button>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<Portal>
|
|
||||||
<TooltipContent side="top" align="center" className="z-[9999]">
|
|
||||||
<p>Configure {name} settings</p>
|
|
||||||
</TooltipContent>
|
|
||||||
</Portal>
|
|
||||||
</Tooltip>
|
|
||||||
</TooltipProvider>
|
|
||||||
)}
|
|
||||||
{showDelete && hasRequiredKeys && isConfigured && (
|
|
||||||
<TooltipProvider>
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
className="rounded-full h-7 w-7 p-0 bg-bgApp hover:bg-bgApp shadow-none text-textSubtle border border-borderSubtle hover:border-borderStandard hover:text-textStandard transition-colors"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
onDelete?.();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<X className="!size-4" />
|
|
||||||
</Button>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<Portal>
|
|
||||||
<TooltipContent side="top" align="center" className="z-[9999]">
|
|
||||||
<p>Remove {name} API Key or Host</p>
|
|
||||||
</TooltipContent>
|
|
||||||
</Portal>
|
|
||||||
</Tooltip>
|
|
||||||
</TooltipProvider>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
{isConfigured && onTakeoff && showTakeoff !== false && (
|
|
||||||
<TooltipProvider>
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<Button
|
|
||||||
data-testid="provider-launch-button"
|
|
||||||
variant="default"
|
|
||||||
size="sm"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
onTakeoff();
|
|
||||||
}}
|
|
||||||
className="rounded-full h-7 px-3 bg-bgApp hover:bg-bgApp shadow-none text-textSubtle border border-borderSubtle hover:border-transparent hover:bg-[#FF9772] hover:text-textProminent transition-colors"
|
|
||||||
>
|
|
||||||
<Rocket className="!size-4" />
|
|
||||||
Launch
|
|
||||||
</Button>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<Portal>
|
|
||||||
<TooltipContent side="top" align="center" className="z-[9999]">
|
|
||||||
<p>Launch goose with {name}</p>
|
|
||||||
</TooltipContent>
|
|
||||||
</Portal>
|
|
||||||
</Tooltip>
|
|
||||||
</TooltipProvider>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
interface BaseProviderGridProps {
|
|
||||||
providers: Provider[];
|
|
||||||
isSelectable?: boolean;
|
|
||||||
showSettings?: boolean;
|
|
||||||
showDelete?: boolean;
|
|
||||||
selectedId?: string | null;
|
|
||||||
onSelect?: (providerId: string) => void;
|
|
||||||
onAddKeys?: (provider: Provider) => void;
|
|
||||||
onConfigure?: (provider: Provider) => void;
|
|
||||||
onDelete?: (provider: Provider) => void;
|
|
||||||
onTakeoff?: (provider: Provider) => void;
|
|
||||||
showTakeoff?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function BaseProviderGrid({
|
|
||||||
providers,
|
|
||||||
isSelectable = false,
|
|
||||||
showSettings = false,
|
|
||||||
showDelete = false,
|
|
||||||
selectedId = null,
|
|
||||||
onSelect,
|
|
||||||
onAddKeys,
|
|
||||||
onConfigure,
|
|
||||||
onDelete,
|
|
||||||
showTakeoff,
|
|
||||||
onTakeoff,
|
|
||||||
}: BaseProviderGridProps) {
|
|
||||||
return (
|
|
||||||
<div className="grid grid-cols-[repeat(auto-fill,_minmax(140px,_1fr))] gap-3 [&_*]:z-20">
|
|
||||||
{providers.map((provider) => {
|
|
||||||
const hasRequiredKeys =
|
|
||||||
(required_keys as Record<string, string[]>)[provider.name]?.length > 0;
|
|
||||||
return (
|
|
||||||
<BaseProviderCard
|
|
||||||
key={provider.id}
|
|
||||||
name={provider.name}
|
|
||||||
description={provider.description}
|
|
||||||
isConfigured={provider.isConfigured}
|
|
||||||
isSelected={selectedId === provider.id}
|
|
||||||
isSelectable={isSelectable}
|
|
||||||
onSelect={() => onSelect?.(provider.id)}
|
|
||||||
onAddKeys={() => onAddKeys?.(provider)}
|
|
||||||
onConfigure={() => onConfigure?.(provider)}
|
|
||||||
onDelete={() => onDelete?.(provider)}
|
|
||||||
onTakeoff={() => onTakeoff?.(provider)}
|
|
||||||
showSettings={showSettings}
|
|
||||||
showDelete={showDelete}
|
|
||||||
hasRequiredKeys={hasRequiredKeys}
|
|
||||||
showTakeoff={showTakeoff}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,308 +0,0 @@
|
|||||||
import { useMemo, useState } from 'react';
|
|
||||||
import { useActiveKeys } from '../api_keys/ActiveKeysContext';
|
|
||||||
import { BaseProviderGrid, getProviderDescription } from './BaseProviderGrid';
|
|
||||||
import { supported_providers, provider_aliases, required_keys } from '../models/hardcoded_stuff';
|
|
||||||
import { ProviderSetupModal } from '../ProviderSetupModal';
|
|
||||||
import { getApiUrl, getSecretKey } from '../../../config';
|
|
||||||
import { getActiveProviders, isSecretKey } from '../api_keys/utils';
|
|
||||||
import { useModel } from '../models/ModelContext';
|
|
||||||
import { Button } from '../../ui/button';
|
|
||||||
import { toastError, toastSuccess } from '../../../toasts';
|
|
||||||
|
|
||||||
function ConfirmationModal({
|
|
||||||
message,
|
|
||||||
onConfirm,
|
|
||||||
onCancel,
|
|
||||||
}: {
|
|
||||||
message: string;
|
|
||||||
onConfirm: () => void;
|
|
||||||
onCancel: () => void;
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<div className="fixed inset-0 bg-black/20 backdrop-blur-sm z-[9999]">
|
|
||||||
<div className="fixed top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[400px] bg-white dark:bg-gray-800 rounded-xl shadow-xl border border-gray-200 dark:border-gray-700">
|
|
||||||
<div className="p-6">
|
|
||||||
<h2 className="text-xl font-semibold text-gray-900 dark:text-gray-100 mb-4">
|
|
||||||
Confirm Delete
|
|
||||||
</h2>
|
|
||||||
<p className="text-sm text-gray-600 dark:text-gray-300 mb-6">{message}</p>
|
|
||||||
<div className="flex justify-end gap-3">
|
|
||||||
<Button variant="outline" onClick={onCancel} className="rounded-full px-4">
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="destructive"
|
|
||||||
onClick={onConfirm}
|
|
||||||
className="rounded-full px-4 bg-red-600 hover:bg-red-700 dark:bg-red-600 dark:hover:bg-red-700"
|
|
||||||
>
|
|
||||||
Delete
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Settings version - non-selectable cards with settings gear
|
|
||||||
export function ConfigureProvidersGrid() {
|
|
||||||
const { activeKeys, setActiveKeys } = useActiveKeys();
|
|
||||||
const [showSetupModal, setShowSetupModal] = useState(false);
|
|
||||||
const [selectedForSetup, setSelectedForSetup] = useState<string | null>(null);
|
|
||||||
const [modalMode, setModalMode] = useState<'edit' | 'setup' | 'battle'>('setup');
|
|
||||||
const [isConfirmationOpen, setIsConfirmationOpen] = useState(false);
|
|
||||||
const [providerToDelete, setProviderToDelete] = useState<{
|
|
||||||
name: string;
|
|
||||||
id: string;
|
|
||||||
isConfigured: boolean;
|
|
||||||
description: string;
|
|
||||||
} | null>(null);
|
|
||||||
const { currentModel } = useModel();
|
|
||||||
|
|
||||||
const providers = useMemo(() => {
|
|
||||||
return supported_providers.map((providerName) => {
|
|
||||||
const alias =
|
|
||||||
provider_aliases.find((p) => p.provider === providerName)?.alias ||
|
|
||||||
providerName.toLowerCase();
|
|
||||||
const isConfigured = activeKeys.includes(providerName);
|
|
||||||
|
|
||||||
return {
|
|
||||||
id: alias,
|
|
||||||
name: providerName,
|
|
||||||
isConfigured,
|
|
||||||
description: getProviderDescription(providerName),
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}, [activeKeys]);
|
|
||||||
|
|
||||||
const handleAddKeys = (provider: {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
isConfigured: boolean;
|
|
||||||
description: string;
|
|
||||||
}) => {
|
|
||||||
setSelectedForSetup(provider.id);
|
|
||||||
setModalMode('setup');
|
|
||||||
setShowSetupModal(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleConfigure = (provider: {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
isConfigured: boolean;
|
|
||||||
description: string;
|
|
||||||
}) => {
|
|
||||||
setSelectedForSetup(provider.id);
|
|
||||||
setModalMode('edit');
|
|
||||||
setShowSetupModal(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleModalSubmit = async (configValues: { [key: string]: string }) => {
|
|
||||||
if (!selectedForSetup) return;
|
|
||||||
|
|
||||||
const provider = providers.find((p) => p.id === selectedForSetup)?.name;
|
|
||||||
if (!provider) return;
|
|
||||||
|
|
||||||
const requiredKeys = (required_keys as Record<string, string[]>)[provider];
|
|
||||||
if (!requiredKeys || requiredKeys.length === 0) {
|
|
||||||
console.error(`No keys found for provider ${provider}`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Delete existing keys if provider is already configured
|
|
||||||
const isUpdate = providers.find((p) => p.id === selectedForSetup)?.isConfigured;
|
|
||||||
if (isUpdate) {
|
|
||||||
for (const keyName of requiredKeys) {
|
|
||||||
const isSecret = isSecretKey(keyName);
|
|
||||||
const deleteResponse = await fetch(getApiUrl('/configs/delete'), {
|
|
||||||
method: 'DELETE',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'X-Secret-Key': getSecretKey(),
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
key: keyName,
|
|
||||||
isSecret,
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!deleteResponse.ok) {
|
|
||||||
const errorText = await deleteResponse.text();
|
|
||||||
console.error('Delete response error:', errorText);
|
|
||||||
throw new Error(`Failed to delete old key: ${keyName}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Store new keys
|
|
||||||
for (const keyName of requiredKeys) {
|
|
||||||
const value = configValues[keyName];
|
|
||||||
if (!value) {
|
|
||||||
console.error(`Missing value for required key: ${keyName}`);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const isSecret = isSecretKey(keyName);
|
|
||||||
const storeResponse = await fetch(getApiUrl('/configs/store'), {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'X-Secret-Key': getSecretKey(),
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
key: keyName,
|
|
||||||
value: value,
|
|
||||||
isSecret,
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!storeResponse.ok) {
|
|
||||||
const errorText = await storeResponse.text();
|
|
||||||
console.error('Store response error:', errorText);
|
|
||||||
throw new Error(`Failed to store new key: ${keyName}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
toastSuccess({
|
|
||||||
title: provider,
|
|
||||||
msg: isUpdate ? `Successfully updated configuration` : `Successfully added configuration`,
|
|
||||||
});
|
|
||||||
|
|
||||||
const updatedKeys = await getActiveProviders();
|
|
||||||
setActiveKeys(updatedKeys);
|
|
||||||
|
|
||||||
setShowSetupModal(false);
|
|
||||||
setSelectedForSetup(null);
|
|
||||||
setModalMode('setup');
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error handling modal submit:', error);
|
|
||||||
toastError({
|
|
||||||
title: provider,
|
|
||||||
msg: `Failed to ${providers.find((p) => p.id === selectedForSetup)?.isConfigured ? 'update' : 'add'} configuration`,
|
|
||||||
traceback: error instanceof Error ? error.message : String(error),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDelete = async (provider: {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
isConfigured: boolean;
|
|
||||||
description: string;
|
|
||||||
}) => {
|
|
||||||
setProviderToDelete(provider);
|
|
||||||
setIsConfirmationOpen(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const confirmDelete = async () => {
|
|
||||||
if (!providerToDelete) return;
|
|
||||||
|
|
||||||
const requiredKeys = required_keys[providerToDelete.name as keyof typeof required_keys];
|
|
||||||
if (!requiredKeys || requiredKeys.length === 0) {
|
|
||||||
console.error(`No keys found for provider ${providerToDelete.name}`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Check if the selected provider is currently active
|
|
||||||
if (currentModel?.provider === providerToDelete.name) {
|
|
||||||
const msg = `Cannot delete the configuration because it's the provider of the current model (${currentModel.name}). Please switch to a different model first.`;
|
|
||||||
toastError({ title: providerToDelete.name, msg, traceback: msg });
|
|
||||||
setIsConfirmationOpen(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete all keys for the provider
|
|
||||||
for (const keyName of requiredKeys) {
|
|
||||||
const isSecret = isSecretKey(keyName);
|
|
||||||
const deleteResponse = await fetch(getApiUrl('/configs/delete'), {
|
|
||||||
method: 'DELETE',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'X-Secret-Key': getSecretKey(),
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
key: keyName,
|
|
||||||
isSecret,
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!deleteResponse.ok) {
|
|
||||||
const errorText = await deleteResponse.text();
|
|
||||||
console.error('Delete response error:', errorText);
|
|
||||||
throw new Error(`Failed to delete key: ${keyName}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('Configuration deleted successfully.');
|
|
||||||
toastSuccess({
|
|
||||||
title: providerToDelete.name,
|
|
||||||
msg: 'Successfully deleted configuration',
|
|
||||||
});
|
|
||||||
|
|
||||||
const updatedKeys = await getActiveProviders();
|
|
||||||
setActiveKeys(updatedKeys);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error deleting configuration:', error);
|
|
||||||
toastError({
|
|
||||||
title: providerToDelete.name,
|
|
||||||
msg: 'Failed to delete configuration',
|
|
||||||
traceback: error instanceof Error ? error.message : String(error),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
setIsConfirmationOpen(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="space-y-4 max-w-[1400px] mx-auto">
|
|
||||||
<BaseProviderGrid
|
|
||||||
providers={providers}
|
|
||||||
showSettings={true}
|
|
||||||
showDelete={true}
|
|
||||||
onAddKeys={handleAddKeys}
|
|
||||||
onConfigure={handleConfigure}
|
|
||||||
onDelete={handleDelete}
|
|
||||||
showTakeoff={false}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{showSetupModal && selectedForSetup && (
|
|
||||||
<div className="relative z-[9999]">
|
|
||||||
<ProviderSetupModal
|
|
||||||
provider={providers.find((p) => p.id === selectedForSetup)?.name || ''}
|
|
||||||
_model="Example Model"
|
|
||||||
_endpoint="Example Endpoint"
|
|
||||||
title={
|
|
||||||
modalMode === 'edit'
|
|
||||||
? `Edit ${providers.find((p) => p.id === selectedForSetup)?.name} Configuration`
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
onSubmit={(configValues) => {
|
|
||||||
if (configValues.forceBattle === 'true') {
|
|
||||||
setSelectedForSetup(selectedForSetup);
|
|
||||||
setModalMode('battle');
|
|
||||||
setShowSetupModal(true);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
handleModalSubmit(configValues);
|
|
||||||
}}
|
|
||||||
onCancel={() => {
|
|
||||||
setShowSetupModal(false);
|
|
||||||
setSelectedForSetup(null);
|
|
||||||
setModalMode('setup');
|
|
||||||
}}
|
|
||||||
forceBattle={modalMode === 'battle'}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{isConfirmationOpen && providerToDelete && (
|
|
||||||
<ConfirmationModal
|
|
||||||
message={`Are you sure you want to delete the configuration for ${providerToDelete.name}? This action cannot be undone.`}
|
|
||||||
onConfirm={confirmDelete}
|
|
||||||
onCancel={() => setIsConfirmationOpen(false)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
import { ScrollArea } from '../../ui/scroll-area';
|
|
||||||
import BackButton from '../../ui/BackButton';
|
|
||||||
import { ConfigureProvidersGrid } from './ConfigureProvidersGrid';
|
|
||||||
import MoreMenuLayout from '../../more_menu/MoreMenuLayout';
|
|
||||||
|
|
||||||
export default function ConfigureProvidersView({ onClose }: { onClose: () => void }) {
|
|
||||||
return (
|
|
||||||
<div className="h-screen w-full">
|
|
||||||
<MoreMenuLayout showMenu={false} />
|
|
||||||
|
|
||||||
<ScrollArea className="h-full w-full">
|
|
||||||
<div className="px-8 pt-6 pb-4">
|
|
||||||
<BackButton onClick={onClose} />
|
|
||||||
<h1 className="text-3xl font-medium text-textStandard mt-1">Configure</h1>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className=" py-8 pt-[20px]">
|
|
||||||
<div className="flex justify-between items-center mb-6 border-b border-borderSubtle px-8">
|
|
||||||
<h2 className="text-xl font-medium text-textStandard">Providers</h2>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Content Area */}
|
|
||||||
<div className="max-w-5xl pt-4 px-8">
|
|
||||||
<div className="relative z-10">
|
|
||||||
<ConfigureProvidersGrid />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</ScrollArea>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -10,8 +10,8 @@ import { DefaultSubmitHandler } from './subcomponents/handlers/DefaultSubmitHand
|
|||||||
import OllamaSubmitHandler from './subcomponents/handlers/OllamaSubmitHandler';
|
import OllamaSubmitHandler from './subcomponents/handlers/OllamaSubmitHandler';
|
||||||
import OllamaForm from './subcomponents/forms/OllamaForm';
|
import OllamaForm from './subcomponents/forms/OllamaForm';
|
||||||
import { useConfig } from '../../../ConfigContext';
|
import { useConfig } from '../../../ConfigContext';
|
||||||
|
import { useModelAndProvider } from '../../../ModelAndProviderContext';
|
||||||
import { AlertTriangle } from 'lucide-react';
|
import { AlertTriangle } from 'lucide-react';
|
||||||
import { getCurrentModelAndProvider } from '../../models'; // Import the utility
|
|
||||||
|
|
||||||
interface FormValues {
|
interface FormValues {
|
||||||
[key: string]: string | number | boolean | null;
|
[key: string]: string | number | boolean | null;
|
||||||
@@ -27,7 +27,8 @@ const customFormsMap: Record<string, unknown> = {
|
|||||||
|
|
||||||
export default function ProviderConfigurationModal() {
|
export default function ProviderConfigurationModal() {
|
||||||
const [validationErrors, setValidationErrors] = useState<Record<string, string>>({});
|
const [validationErrors, setValidationErrors] = useState<Record<string, string>>({});
|
||||||
const { upsert, remove, read } = useConfig(); // Add read to the destructured values
|
const { upsert, remove } = useConfig();
|
||||||
|
const { getCurrentModelAndProvider } = useModelAndProvider();
|
||||||
const { isOpen, currentProvider, modalProps, closeModal } = useProviderModal();
|
const { isOpen, currentProvider, modalProps, closeModal } = useProviderModal();
|
||||||
const [configValues, setConfigValues] = useState<Record<string, string>>({});
|
const [configValues, setConfigValues] = useState<Record<string, string>>({});
|
||||||
const [showDeleteConfirmation, setShowDeleteConfirmation] = useState(false);
|
const [showDeleteConfirmation, setShowDeleteConfirmation] = useState(false);
|
||||||
@@ -126,10 +127,7 @@ export default function ProviderConfigurationModal() {
|
|||||||
const handleDelete = async () => {
|
const handleDelete = async () => {
|
||||||
// Check if this is the currently active provider
|
// Check if this is the currently active provider
|
||||||
try {
|
try {
|
||||||
const providerModel = await getCurrentModelAndProvider({
|
const providerModel = await getCurrentModelAndProvider();
|
||||||
readFromConfig: read,
|
|
||||||
writeToConfig: upsert,
|
|
||||||
});
|
|
||||||
if (currentProvider.name === providerModel.provider) {
|
if (currentProvider.name === providerModel.provider) {
|
||||||
// It's the active provider - set state and show warning
|
// It's the active provider - set state and show warning
|
||||||
setIsActiveProvider(true);
|
setIsActiveProvider(true);
|
||||||
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 3.2 KiB |
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 3.4 KiB After Width: | Height: | Size: 3.4 KiB |
|
Before Width: | Height: | Size: 5.0 KiB After Width: | Height: | Size: 5.0 KiB |
|
Before Width: | Height: | Size: 951 B After Width: | Height: | Size: 951 B |
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 2.4 KiB |
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 2.4 KiB |
|
Before Width: | Height: | Size: 3.6 KiB After Width: | Height: | Size: 3.6 KiB |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |