UI update with sidebar and settings tabs (#3288)

Co-authored-by: Nahiyan Khan <nahiyan@squareup.com>
Co-authored-by: Taylor Ho <taylorkmho@gmail.com>
Co-authored-by: Lily Delalande <119957291+lily-de@users.noreply.github.com>
Co-authored-by: Spence <spencrmartin@gmail.com>
Co-authored-by: spencrmartin <spencermartin@squareup.com>
Co-authored-by: Judson Stephenson <Jud@users.noreply.github.com>
Co-authored-by: Max Novich <mnovich@squareup.com>
Co-authored-by: Best Codes <106822363+The-Best-Codes@users.noreply.github.com>
Co-authored-by: caroline-a-mckenzie <cmckenzie@squareup.com>
Co-authored-by: Michael Neale <michael.neale@gmail.com>
This commit is contained in:
Zane
2025-07-15 17:24:41 -07:00
committed by GitHub
parent b22f50d1a1
commit 77ea27f5f5
195 changed files with 20633 additions and 7238 deletions
@@ -2,9 +2,12 @@ import { useEffect, useState, useCallback } from 'react';
import type { View } from '../../../App';
import ModelSettingsButtons from './subcomponents/ModelSettingsButtons';
import { useConfig } from '../../ConfigContext';
import { useModelAndProvider } from '../../ModelAndProviderContext';
import { toastError } from '../../../toasts';
import { UNKNOWN_PROVIDER_MSG, UNKNOWN_PROVIDER_TITLE } from './index';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../../ui/card';
import ResetProviderSection from '../reset_provider/ResetProviderSection';
interface ModelsSectionProps {
setView: (view: View) => void;
@@ -12,35 +15,47 @@ interface ModelsSectionProps {
export default function ModelsSection({ setView }: ModelsSectionProps) {
const [provider, setProvider] = useState<string | null>(null);
const [model, setModel] = useState<string>('');
const [displayModelName, setDisplayModelName] = useState<string>('');
const [isLoading, setIsLoading] = useState<boolean>(true);
const { read, getProviders } = useConfig();
const { getCurrentModelDisplayName, getCurrentProviderDisplayName } = useModelAndProvider();
// Function to load model data
const loadModelData = useCallback(async () => {
try {
const gooseModel = (await read('GOOSE_MODEL', false)) as string;
setIsLoading(true);
const gooseProvider = (await read('GOOSE_PROVIDER', false)) as string;
const providers = await getProviders(true);
// lookup display name
const providerDetailsList = providers.filter((provider) => provider.name === gooseProvider);
// Get display name (alias if available, otherwise model name)
const modelDisplayName = await getCurrentModelDisplayName();
setDisplayModelName(modelDisplayName);
if (providerDetailsList.length != 1) {
toastError({
title: UNKNOWN_PROVIDER_TITLE,
msg: UNKNOWN_PROVIDER_MSG,
});
setModel(gooseModel);
setProvider(gooseProvider);
} else {
const providerDisplayName = providerDetailsList[0].metadata.display_name;
setModel(gooseModel);
// Get provider display name (subtext if available from predefined models, otherwise provider metadata)
const providerDisplayName = await getCurrentProviderDisplayName();
if (providerDisplayName) {
setProvider(providerDisplayName);
} else {
// Fallback to original provider lookup
const providerDetailsList = providers.filter((provider) => provider.name === gooseProvider);
if (providerDetailsList.length != 1) {
toastError({
title: UNKNOWN_PROVIDER_TITLE,
msg: UNKNOWN_PROVIDER_MSG,
});
setProvider(gooseProvider);
} else {
const fallbackProviderDisplayName = providerDetailsList[0].metadata.display_name;
setProvider(fallbackProviderDisplayName);
}
}
} catch (error) {
console.error('Error loading model data:', error);
} finally {
setIsLoading(false);
}
}, [read, getProviders]);
}, [read, getProviders, getCurrentModelDisplayName, getCurrentProviderDisplayName]);
useEffect(() => {
loadModelData();
@@ -48,17 +63,34 @@ export default function ModelsSection({ setView }: ModelsSectionProps) {
}, []);
return (
<section id="models" className="px-8">
<div className="flex justify-between items-center mb-4">
<h2 className="text-xl font-medium text-textStandard">Models</h2>
</div>
<div className="border-b border-borderSubtle pb-8">
<div className="">
<h3 className="text-textStandard">{model}</h3>
<h4 className="text-xs text-textSubtle">{provider}</h4>
</div>
<ModelSettingsButtons setView={setView} />
</div>
<section id="models" className="space-y-4 pr-4">
<Card className="p-2 pb-4">
<CardContent className="px-2">
{isLoading ? (
<>
<div className="h-[20px] mb-1"></div>
<div className="h-[16px]"></div>
</>
) : (
<div className="animate-in fade-in duration-100">
<h3 className="text-text-default">{displayModelName}</h3>
<h4 className="text-xs text-text-muted">{provider}</h4>
</div>
)}
<ModelSettingsButtons setView={setView} />
</CardContent>
</Card>
<Card className="pb-2 rounded-lg">
<CardHeader className="pb-0">
<CardTitle className="">Reset Provider and Model</CardTitle>
<CardDescription>
Clear your selected model and provider settings to start fresh
</CardDescription>
</CardHeader>
<CardContent className="px-2">
<ResetProviderSection setView={setView} />
</CardContent>
</Card>
</section>
);
}
@@ -1,33 +1,62 @@
import { Sliders } from 'lucide-react';
import React, { useEffect, useState, useRef } from 'react';
import { Sliders, ChefHat, Bot, Eye, Save } from 'lucide-react';
import React, { useEffect, useState } from 'react';
import { useModelAndProvider } from '../../../ModelAndProviderContext';
import { AddModelModal } from '../subcomponents/AddModelModal';
import { LeadWorkerSettings } from '../subcomponents/LeadWorkerSettings';
import { View } from '../../../../App';
import { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } from '../../../ui/Tooltip';
import Modal from '../../../Modal';
import { useCurrentModelInfo } from '../../../ChatView';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
DropdownMenuSeparator,
} from '../../../ui/dropdown-menu';
import { useCurrentModelInfo } from '../../../BaseChat';
import { useConfig } from '../../../ConfigContext';
import { Alert } from '../../../alerts';
import BottomMenuAlertPopover from '../../../bottom_menu/BottomMenuAlertPopover';
import { Recipe } from '../../../../recipe';
import { saveRecipe, generateRecipeFilename } from '../../../../recipe/recipeStorage';
import { toastSuccess, toastError } from '../../../../toasts';
import ViewRecipeModal from '../../../ViewRecipeModal';
interface ModelsBottomBarProps {
dropdownRef: React.RefObject<HTMLDivElement>;
setView: (view: View) => void;
alerts: Alert[];
recipeConfig?: Recipe | null;
hasMessages?: boolean; // Add prop to know if there are messages to create a recipe from
}
export default function ModelsBottomBar({ dropdownRef, setView }: ModelsBottomBarProps) {
const { currentModel, currentProvider, getCurrentModelAndProviderForDisplay } =
useModelAndProvider();
export default function ModelsBottomBar({
dropdownRef,
setView,
alerts,
recipeConfig,
hasMessages = false,
}: ModelsBottomBarProps) {
const {
currentModel,
currentProvider,
getCurrentModelAndProviderForDisplay,
getCurrentModelDisplayName,
getCurrentProviderDisplayName,
} = useModelAndProvider();
const currentModelInfo = useCurrentModelInfo();
const { read } = useConfig();
const [isModelMenuOpen, setIsModelMenuOpen] = useState(false);
const [displayProvider, setDisplayProvider] = useState<string | null>(null);
const [displayModelName, setDisplayModelName] = useState<string>('Select Model');
const [isAddModelModalOpen, setIsAddModelModalOpen] = useState(false);
const [isLeadWorkerModalOpen, setIsLeadWorkerModalOpen] = useState(false);
const [isLeadWorkerActive, setIsLeadWorkerActive] = useState(false);
const menuRef = useRef<HTMLDivElement>(null);
const [isModelTruncated, setIsModelTruncated] = useState(false);
// eslint-disable-next-line no-undef
const modelRef = useRef<HTMLSpanElement>(null);
const [isTooltipOpen, setIsTooltipOpen] = useState(false);
// Save recipe dialog state (like in RecipeEditor.tsx)
const [showSaveDialog, setShowSaveDialog] = useState(false);
const [saveRecipeName, setSaveRecipeName] = useState('');
const [saveGlobal, setSaveGlobal] = useState(true);
const [saving, setSaving] = useState(false);
// View recipe modal state
const [showViewRecipeModal, setShowViewRecipeModal] = useState(false);
// Check if lead/worker mode is active
useEffect(() => {
@@ -36,139 +65,298 @@ export default function ModelsBottomBar({ dropdownRef, setView }: ModelsBottomBa
const leadModel = await read('GOOSE_LEAD_MODEL', false);
setIsLeadWorkerActive(!!leadModel);
} catch (error) {
console.error('Error checking lead model:', error);
setIsLeadWorkerActive(false);
}
};
checkLeadWorker();
}, [read]);
// Refresh lead/worker status when modal closes
const handleLeadWorkerModalClose = () => {
setIsLeadWorkerModalOpen(false);
// Refresh the lead/worker status after modal closes
const checkLeadWorker = async () => {
try {
const leadModel = await read('GOOSE_LEAD_MODEL', false);
const currentModel = await read('GOOSE_MODEL', false);
setIsLeadWorkerActive(!!leadModel);
setLeadModelName((leadModel as string) || '');
setCurrentActiveModel((currentModel as string) || '');
} catch (error) {
console.error('Error checking lead model after modal close:', error);
setIsLeadWorkerActive(false);
}
};
checkLeadWorker();
};
// Determine which model to display - activeModel takes priority when lead/worker is active
const displayModel =
isLeadWorkerActive && currentModelInfo?.model
? currentModelInfo.model
: currentModel || 'Select Model';
const modelMode = currentModelInfo?.mode;
isLeadWorkerActive && currentModelInfo?.model ? currentModelInfo.model : displayModelName;
// Since currentModelInfo.mode is not working, let's determine mode differently
// We'll need to get the lead model and compare it with the current model
const [leadModelName, setLeadModelName] = useState<string>('');
const [currentActiveModel, setCurrentActiveModel] = useState<string>('');
// Get lead model name and current model for comparison
useEffect(() => {
const getModelInfo = async () => {
try {
const leadModel = await read('GOOSE_LEAD_MODEL', false);
const currentModel = await read('GOOSE_MODEL', false);
setLeadModelName((leadModel as string) || '');
setCurrentActiveModel((currentModel as string) || '');
} catch (error) {
console.error('Error getting model info:', error);
}
};
getModelInfo();
}, [read]);
// Determine the mode based on which model is currently active
const modelMode = isLeadWorkerActive
? currentActiveModel === leadModelName
? 'lead'
: 'worker'
: undefined;
// Update display provider when current provider changes
useEffect(() => {
if (currentProvider) {
(async () => {
const modelProvider = await getCurrentModelAndProviderForDisplay();
setDisplayProvider(modelProvider.provider);
const providerDisplayName = await getCurrentProviderDisplayName();
if (providerDisplayName) {
setDisplayProvider(providerDisplayName);
} else {
const modelProvider = await getCurrentModelAndProviderForDisplay();
setDisplayProvider(modelProvider.provider);
}
})();
}
}, [currentProvider, getCurrentModelAndProviderForDisplay]);
}, [currentProvider, getCurrentProviderDisplayName, getCurrentModelAndProviderForDisplay]);
// Update display model name when current model changes
useEffect(() => {
const checkTruncation = () => {
if (modelRef.current) {
setIsModelTruncated(modelRef.current.scrollWidth > modelRef.current.clientWidth);
}
};
checkTruncation();
window.addEventListener('resize', checkTruncation);
return () => window.removeEventListener('resize', checkTruncation);
}, [displayModel]);
(async () => {
const displayName = await getCurrentModelDisplayName();
setDisplayModelName(displayName);
})();
}, [currentModel, getCurrentModelDisplayName]);
useEffect(() => {
setIsTooltipOpen(false);
}, [isModelTruncated]);
// Handle view recipe - open modal instead of navigating
const handleViewRecipe = () => {
if (recipeConfig) {
setShowViewRecipeModal(true);
}
};
// Add click outside handler
useEffect(() => {
function handleClickOutside(event: MouseEvent) {
if (menuRef.current && !menuRef.current.contains(event.target as Node)) {
setIsModelMenuOpen(false);
}
// Handle save recipe - show save dialog (like in RecipeEditor.tsx)
const handleSaveRecipeClick = () => {
if (recipeConfig) {
const suggestedName = generateRecipeFilename(recipeConfig);
setSaveRecipeName(suggestedName);
setShowSaveDialog(true);
}
};
// Handle save recipe (like in RecipeEditor.tsx)
const handleSaveRecipe = async () => {
if (!saveRecipeName.trim() || !recipeConfig) {
return;
}
// Add the event listener when the menu is open
if (isModelMenuOpen) {
document.addEventListener('mousedown', handleClickOutside);
}
setSaving(true);
try {
if (!recipeConfig.title || !recipeConfig.description || !recipeConfig.instructions) {
throw new Error('Invalid recipe configuration: missing required fields');
}
// Clean up the event listener
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
}, [isModelMenuOpen]);
await saveRecipe(recipeConfig, {
name: saveRecipeName.trim(),
global: saveGlobal,
});
// Reset dialog state
setShowSaveDialog(false);
setSaveRecipeName('');
toastSuccess({
title: saveRecipeName.trim(),
msg: `Recipe saved successfully`,
});
} catch (error) {
console.error('Failed to save recipe:', error);
toastError({
title: 'Save Failed',
msg: `Failed to save recipe: ${error instanceof Error ? error.message : 'Unknown error'}`,
traceback: error instanceof Error ? error.message : String(error),
});
} finally {
setSaving(false);
}
};
return (
<div className="relative flex items-center" ref={dropdownRef}>
<div ref={menuRef} className="relative">
<div
className="flex items-center hover:cursor-pointer max-w-[180px] md:max-w-[200px] lg:max-w-[380px] min-w-0 group hover:text-textStandard transition-colors"
onClick={() => setIsModelMenuOpen(!isModelMenuOpen)}
>
<TooltipProvider>
<Tooltip open={isTooltipOpen} onOpenChange={setIsTooltipOpen}>
<TooltipTrigger asChild>
<span
ref={modelRef}
className="truncate max-w-[130px] md:max-w-[200px] lg:max-w-[360px] min-w-0 block"
>
{displayModel}
{isLeadWorkerActive && modelMode && (
<span className="ml-1 text-[10px] opacity-60">({modelMode})</span>
)}
</span>
</TooltipTrigger>
{isModelTruncated && (
<TooltipContent className="max-w-96 overflow-auto scrollbar-thin" side="top">
{displayModel}
{isLeadWorkerActive && modelMode && (
<span className="ml-1 text-[10px] opacity-60">({modelMode})</span>
)}
</TooltipContent>
<BottomMenuAlertPopover alerts={alerts} />
<DropdownMenu>
<DropdownMenuTrigger className="flex items-center hover:cursor-pointer max-w-[180px] md:max-w-[200px] lg:max-w-[380px] min-w-0 text-text-default/70 hover:text-text-default transition-colors">
<div className="flex items-center truncate max-w-[130px] md:max-w-[200px] lg:max-w-[360px] min-w-0">
<Bot className="mr-1 h-4 w-4 flex-shrink-0" />
<span className="truncate text-xs">
{displayModel}
{isLeadWorkerActive && modelMode && (
<span className="ml-1 text-[10px] opacity-60">({modelMode})</span>
)}
</Tooltip>
</TooltipProvider>
</div>
{/* Dropdown Menu */}
{isModelMenuOpen && (
<div className="absolute bottom-[24px] right-[-55px] w-[300px] bg-bgApp rounded-lg border border-borderSubtle">
<div className="">
<div className="text-sm text-textProminent mt-2 ml-2">Current:</div>
<div className="flex items-center justify-between text-sm ml-2">
{currentModel} -- {displayProvider}
</div>
<div
className="flex items-center justify-between text-textStandard p-2 cursor-pointer transition-colors hover:bg-bgStandard
border-t border-borderSubtle mt-2"
onClick={() => {
setIsModelMenuOpen(false);
setIsAddModelModalOpen(true);
}}
>
<span className="text-sm">Change Model</span>
<Sliders className="w-4 h-4 ml-2 rotate-90" />
</div>
<div
className="flex items-center justify-between text-textStandard p-2 cursor-pointer transition-colors hover:bg-bgStandard
border-t border-borderSubtle"
onClick={() => {
setIsModelMenuOpen(false);
setIsLeadWorkerModalOpen(true);
}}
>
<span className="text-sm">Lead/Worker Settings</span>
<Sliders className="w-4 h-4 ml-2" />
</div>
</div>
</span>
</div>
)}
</div>
</DropdownMenuTrigger>
<DropdownMenuContent side="top" align="center" className="w-64 text-sm">
<h6 className="text-xs text-textProminent mt-2 ml-2">Current model</h6>
<p className="flex items-center justify-between text-sm mx-2 pb-2 border-b mb-2">
{displayModelName}
{displayProvider && `${displayProvider}`}
</p>
<DropdownMenuItem onClick={() => setIsAddModelModalOpen(true)}>
<span>Change Model</span>
<Sliders className="ml-auto h-4 w-4 rotate-90" />
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setIsLeadWorkerModalOpen(true)}>
<span>Lead/Worker Settings</span>
<Sliders className="ml-auto h-4 w-4" />
</DropdownMenuItem>
{/* Recipe-specific menu items - only show when actively using a recipe */}
{recipeConfig && (
<>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={handleViewRecipe}>
<span>View Recipe</span>
<Eye className="ml-auto h-4 w-4" />
</DropdownMenuItem>
<DropdownMenuItem onClick={handleSaveRecipeClick}>
<span>Save Recipe</span>
<Save className="ml-auto h-4 w-4" />
</DropdownMenuItem>
</>
)}
<DropdownMenuSeparator />
{/* Only show "Create a recipe from this session" when there are messages to create from */}
{hasMessages && (
<DropdownMenuItem
onClick={() => {
// Signal to create an agent from the current chat
window.dispatchEvent(new CustomEvent('make-agent-from-chat'));
}}
>
<span>Create a recipe from this session</span>
<ChefHat className="ml-auto h-4 w-4" />
</DropdownMenuItem>
)}
</DropdownMenuContent>
</DropdownMenu>
{isAddModelModalOpen ? (
<AddModelModal setView={setView} onClose={() => setIsAddModelModalOpen(false)} />
) : null}
{isLeadWorkerModalOpen ? (
<Modal onClose={() => setIsLeadWorkerModalOpen(false)}>
<LeadWorkerSettings onClose={() => setIsLeadWorkerModalOpen(false)} />
</Modal>
<LeadWorkerSettings isOpen={isLeadWorkerModalOpen} onClose={handleLeadWorkerModalClose} />
) : null}
{/* Save Recipe Dialog - copied from RecipeEditor.tsx */}
{showSaveDialog && (
<div className="fixed inset-0 z-[300] flex items-center justify-center bg-black/50">
<div className="bg-background-default border border-borderSubtle rounded-lg p-6 w-96 max-w-[90vw]">
<h3 className="text-lg font-medium text-textProminent mb-4">Save Recipe</h3>
<div className="space-y-4">
<div>
<label
htmlFor="recipe-name"
className="block text-sm font-medium text-textStandard mb-2"
>
Recipe Name
</label>
<input
id="recipe-name"
type="text"
value={saveRecipeName}
onChange={(e) => setSaveRecipeName(e.target.value)}
className="w-full p-3 border border-borderSubtle rounded-lg bg-background-default text-textStandard focus:outline-none focus:ring-2 focus:ring-borderProminent"
placeholder="Enter recipe name"
autoFocus
/>
</div>
<div>
<label className="block text-sm font-medium text-textStandard mb-2">
Save Location
</label>
<div className="space-y-2">
<label className="flex items-center">
<input
type="radio"
name="save-location"
checked={saveGlobal}
onChange={() => setSaveGlobal(true)}
className="mr-2"
/>
<span className="text-sm text-textStandard">
Global - Available across all Goose sessions
</span>
</label>
<label className="flex items-center">
<input
type="radio"
name="save-location"
checked={!saveGlobal}
onChange={() => setSaveGlobal(false)}
className="mr-2"
/>
<span className="text-sm text-textStandard">
Directory - Available in the working directory
</span>
</label>
</div>
</div>
</div>
<div className="flex justify-end space-x-3 mt-6">
<button
onClick={() => {
setShowSaveDialog(false);
setSaveRecipeName('');
}}
className="px-4 py-2 text-textSubtle hover:text-textStandard transition-colors"
disabled={saving}
>
Cancel
</button>
<button
onClick={handleSaveRecipe}
disabled={!saveRecipeName.trim() || saving}
className="px-4 py-2 bg-textProminent text-bgApp rounded-lg hover:bg-opacity-90 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
>
{saving ? 'Saving...' : 'Save Recipe'}
</button>
</div>
</div>
</div>
)}
{/* View Recipe Modal */}
{recipeConfig && (
<ViewRecipeModal
isOpen={showViewRecipeModal}
onClose={() => setShowViewRecipeModal(false)}
config={recipeConfig}
/>
)}
</div>
);
}
@@ -0,0 +1,30 @@
import Model from './modelInterface';
// Helper functions for predefined models - shared across components
export function getPredefinedModelsFromEnv(): Model[] {
try {
const envModels = window.appConfig.get('GOOSE_PREDEFINED_MODELS'); // process.env.GOOSE_PREDEFINED_MODELS
if (envModels && typeof envModels === 'string') {
return JSON.parse(envModels) as Model[];
}
} catch (error) {
console.warn('Failed to parse GOOSE_PREDEFINED_MODELS environment variable:', error);
}
return [];
}
export function shouldShowPredefinedModels(): boolean {
return getPredefinedModelsFromEnv().length > 0;
}
export function getModelDisplayName(modelName: string): string {
const predefinedModels = getPredefinedModelsFromEnv();
const matchingModel = predefinedModels.find((model) => model.name === modelName);
return matchingModel?.alias || modelName;
}
export function getProviderDisplayName(modelName: string): string {
const predefinedModels = getPredefinedModelsFromEnv();
const matchingModel = predefinedModels.find((model) => model.name === modelName);
return matchingModel?.subtext || '';
}
@@ -1,28 +0,0 @@
import { useState } from 'react';
import { Button } from '../../../ui/button';
import { AddModelModal } from './AddModelModal';
import type { View } from '../../../../App';
import { ArrowLeftRight } from 'lucide-react';
interface AddModelButtonProps {
setView: (view: View) => void;
}
export const AddModelButton = ({ setView }: AddModelButtonProps) => {
const [isAddModelModalOpen, setIsAddModelModalOpen] = useState(false);
return (
<>
<Button
className="flex items-center gap-2 justify-center text-white dark:text-black bg-bgAppInverse hover:bg-bgStandardInverse [&>svg]:!size-4"
onClick={() => setIsAddModelModalOpen(true)}
>
<ArrowLeftRight />
Switch models
</Button>
{isAddModelModalOpen ? (
<AddModelModal setView={setView} onClose={() => setIsAddModelModalOpen(false)} />
) : null}
</>
);
};
@@ -1,7 +1,14 @@
import { useEffect, useState, useCallback } from 'react';
import { ArrowLeftRight, ExternalLink } from 'lucide-react';
import Modal from '../../../Modal';
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '../../../ui/dialog';
import { Button } from '../../../ui/button';
import { QUICKSTART_GUIDE_URL } from '../../providers/modal/constants';
import { Input } from '../../../ui/input';
@@ -10,44 +17,14 @@ import { useConfig } from '../../../ConfigContext';
import { useModelAndProvider } from '../../../ModelAndProviderContext';
import type { View } from '../../../../App';
import Model, { getProviderMetadata } from '../modelInterface';
const ModalButtons = ({
onSubmit,
onCancel,
_isValid: _,
_validationErrors: __,
}: {
onSubmit: () => void;
onCancel: () => void;
_isValid: boolean;
_validationErrors: { provider: string; model: string };
}) => (
<div>
<Button
type="submit"
variant="ghost"
onClick={onSubmit}
className="w-full h-[60px] rounded-none border-borderSubtle text-base hover:bg-bgSubtle text-textProminent font-regular"
>
Select model
</Button>
<Button
type="button"
variant="ghost"
onClick={onCancel}
className="w-full h-[60px] rounded-none border-t border-borderSubtle hover:text-textStandard text-textSubtle hover:bg-bgSubtle text-base font-regular"
>
Cancel
</Button>
</div>
);
import { getPredefinedModelsFromEnv, shouldShowPredefinedModels } from '../predefinedModelsUtils';
type AddModelModalProps = {
onClose: () => void;
setView: (view: View) => void;
};
export const AddModelModal = ({ onClose, setView }: AddModelModalProps) => {
const { getProviders } = useConfig();
const { getProviders, read } = useConfig();
const { changeModel } = useModelAndProvider();
const [providerOptions, setProviderOptions] = useState<{ value: string; label: string }[]>([]);
const [modelOptions, setModelOptions] = useState<
@@ -62,6 +39,9 @@ export const AddModelModal = ({ onClose, setView }: AddModelModalProps) => {
});
const [isValid, setIsValid] = useState(true);
const [attemptedSubmit, setAttemptedSubmit] = useState(false);
const [usePredefinedModels] = useState(shouldShowPredefinedModels());
const [selectedPredefinedModel, setSelectedPredefinedModel] = useState<Model | null>(null);
const [predefinedModels, setPredefinedModels] = useState<Model[]>([]);
// Validate form data
const validateForm = useCallback(() => {
@@ -71,33 +51,48 @@ export const AddModelModal = ({ onClose, setView }: AddModelModalProps) => {
};
let formIsValid = true;
if (!provider) {
errors.provider = 'Please select a provider';
formIsValid = false;
}
if (usePredefinedModels) {
if (!selectedPredefinedModel) {
errors.model = 'Please select a model';
formIsValid = false;
}
} else {
if (!provider) {
errors.provider = 'Please select a provider';
formIsValid = false;
}
if (!model) {
errors.model = 'Please select or enter a model';
formIsValid = false;
if (!model) {
errors.model = 'Please select or enter a model';
formIsValid = false;
}
}
setValidationErrors(errors);
setIsValid(formIsValid);
return formIsValid;
}, [model, provider]);
}, [model, provider, usePredefinedModels, selectedPredefinedModel]);
const onSubmit = async () => {
const handleClose = () => {
onClose();
};
const handleSubmit = async () => {
setAttemptedSubmit(true);
const isFormValid = validateForm();
if (isFormValid) {
const providerMetaData = await getProviderMetadata(provider || '', getProviders);
const providerDisplayName = providerMetaData.display_name;
let modelObj: Model;
const modelObj = { name: model, provider: provider, subtext: providerDisplayName } as Model;
if (usePredefinedModels && selectedPredefinedModel) {
modelObj = selectedPredefinedModel;
} else {
const providerMetaData = await getProviderMetadata(provider || '', getProviders);
const providerDisplayName = providerMetaData.display_name;
modelObj = { name: model, provider: provider, subtext: providerDisplayName } as Model;
}
await changeModel(modelObj);
onClose();
}
};
@@ -110,6 +105,26 @@ export const AddModelModal = ({ onClose, setView }: AddModelModalProps) => {
}, [attemptedSubmit, validateForm]);
useEffect(() => {
// Load predefined models if enabled
if (usePredefinedModels) {
const models = getPredefinedModelsFromEnv();
setPredefinedModels(models);
// Initialize selected predefined model with current model
(async () => {
try {
const currentModelName = (await read('GOOSE_MODEL', false)) as string;
const matchingModel = models.find((model) => model.name === currentModelName);
if (matchingModel) {
setSelectedPredefinedModel(matchingModel);
}
} catch (error) {
console.error('Failed to get current model for selection:', error);
}
})();
}
// Load providers for manual model selection
(async () => {
try {
const providersResponse = await getProviders(false);
@@ -157,7 +172,7 @@ export const AddModelModal = ({ onClose, setView }: AddModelModalProps) => {
console.error('Failed to load providers:', error);
}
})();
}, [getProviders]);
}, [getProviders, usePredefinedModels, read]);
// Filter model options based on selected provider
const filteredModelOptions = provider
@@ -223,106 +238,172 @@ export const AddModelModal = ({ onClose, setView }: AddModelModalProps) => {
};
return (
<div className="z-10">
<Modal
onClose={onClose}
footer={
<ModalButtons
onSubmit={onSubmit}
onCancel={onClose}
_isValid={isValid}
_validationErrors={validationErrors}
/>
}
>
<div className="flex flex-col items-center gap-8">
<div className="flex flex-col gap-3">
<Dialog open={true} onOpenChange={handleClose}>
<DialogContent className="sm:max-w-[500px]">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<ArrowLeftRight size={24} className="text-textStandard" />
<div className="text-textStandard font-medium text-base">Switch models</div>
<div className="text-textSubtle text-md">
Configure your AI model providers by adding their API keys. Your keys are stored
securely and encrypted locally.
</div>
<div>
<a
href={QUICKSTART_GUIDE_URL}
target="_blank"
rel="noopener noreferrer"
className="flex items-center text-textStandard font-medium text-sm"
>
<ExternalLink size={16} className="mr-1" />
View quick start guide
</a>
</div>
Switch models
</DialogTitle>
<DialogDescription>
Configure your AI model providers by adding their API keys. Your keys are stored
securely and encrypted locally.
</DialogDescription>
</DialogHeader>
<div className="flex flex-col gap-4 py-4">
<div>
<a
href={QUICKSTART_GUIDE_URL}
target="_blank"
rel="noopener noreferrer"
className="flex items-center text-textStandard font-medium text-sm"
>
<ExternalLink size={16} className="mr-1" />
View quick start guide
</a>
</div>
<div className="w-full flex flex-col gap-4">
<div>
<Select
options={providerOptions}
value={providerOptions.find((option) => option.value === provider) || null}
onChange={(newValue: unknown) => {
const option = newValue as { value: string; label: string } | null;
if (option?.value === 'configure_providers') {
// Navigate to ConfigureProviders view
setView('ConfigureProviders');
onClose(); // Close the current modal
} else {
setProvider(option?.value || null);
setModel('');
setIsCustomModel(false);
}
}}
placeholder="Provider"
isClearable
/>
{attemptedSubmit && validationErrors.provider && (
<div className="text-red-500 text-sm mt-1">{validationErrors.provider}</div>
{usePredefinedModels ? (
/* Predefined Models Section */
<div className="w-full flex flex-col gap-4">
<div className="flex justify-between items-center">
<label className="text-sm font-medium text-textStandard">Choose a model:</label>
</div>
<div className="space-y-2 max-h-64 overflow-y-auto">
{predefinedModels.map((model) => (
<div key={model.id || model.name} className="group hover:cursor-pointer text-sm">
<div
className={`flex items-center justify-between text-text-default py-2 px-2 ${
selectedPredefinedModel?.name === model.name
? 'bg-background-muted'
: 'bg-background-default hover:bg-background-muted'
} rounded-lg transition-all`}
onClick={() => setSelectedPredefinedModel(model)}
>
<div className="flex-1">
<div className="flex items-center justify-between">
<span className="text-text-default font-medium">
{model.alias || model.name}
</span>
{model.alias?.includes('recommended') && (
<span className="text-xs bg-background-muted text-textStandard px-2 py-1 rounded-full border border-borderSubtle ml-2">
Recommended
</span>
)}
</div>
<div className="flex items-center gap-2 mt-[2px]">
<span className="text-xs text-text-muted">{model.subtext}</span>
<span className="text-xs text-text-muted"></span>
<span className="text-xs text-text-muted">{model.provider}</span>
</div>
</div>
<div className="relative flex items-center ml-3">
<input
type="radio"
name="predefined-model"
value={model.name}
checked={selectedPredefinedModel?.name === model.name}
onChange={() => setSelectedPredefinedModel(model)}
className="peer sr-only"
/>
<div
className="h-4 w-4 rounded-full border border-border-default
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 group-hover:border-border-default"
></div>
</div>
</div>
</div>
))}
</div>
{attemptedSubmit && validationErrors.model && (
<div className="text-red-500 text-sm mt-1">{validationErrors.model}</div>
)}
</div>
{provider && (
<>
{!isCustomModel ? (
<div>
<Select
options={filteredModelOptions}
onChange={handleModelChange}
onInputChange={handleInputChange} // Added for input handling
value={model ? { value: model, label: model } : null}
placeholder="Select a model"
/>
{attemptedSubmit && validationErrors.model && (
<div className="text-red-500 text-sm mt-1">{validationErrors.model}</div>
)}
</div>
) : (
<div className="flex flex-col gap-2">
<div className="flex justify-between">
<label className="text-sm text-textSubtle">Custom model name</label>
<button
onClick={() => setIsCustomModel(false)}
className="text-sm text-textSubtle"
>
Back to model list
</button>
</div>
<Input
className="border-2 px-4 py-5"
placeholder="Type model name here"
onChange={(event) => setModel(event.target.value)}
value={model}
/>
{attemptedSubmit && validationErrors.model && (
<div className="text-red-500 text-sm mt-1">{validationErrors.model}</div>
)}
</div>
) : (
/* Manual Provider/Model Selection */
<div className="w-full flex flex-col gap-4">
<div>
<Select
options={providerOptions}
value={providerOptions.find((option) => option.value === provider) || null}
onChange={(newValue: unknown) => {
const option = newValue as { value: string; label: string } | null;
if (option?.value === 'configure_providers') {
// Navigate to ConfigureProviders view
setView('ConfigureProviders');
onClose(); // Close the current modal
} else {
setProvider(option?.value || null);
setModel('');
setIsCustomModel(false);
}
}}
placeholder="Provider"
isClearable
/>
{attemptedSubmit && validationErrors.provider && (
<div className="text-red-500 text-sm mt-1">{validationErrors.provider}</div>
)}
</>
)}
</div>
</div>
{provider && (
<>
{!isCustomModel ? (
<div>
<Select
options={filteredModelOptions}
onChange={handleModelChange}
onInputChange={handleInputChange} // Added for input handling
value={model ? { value: model, label: model } : null}
placeholder="Select a model"
/>
{attemptedSubmit && validationErrors.model && (
<div className="text-red-500 text-sm mt-1">{validationErrors.model}</div>
)}
</div>
) : (
<div className="flex flex-col gap-2">
<div className="flex justify-between">
<label className="text-sm text-textSubtle">Custom model name</label>
<button
onClick={() => setIsCustomModel(false)}
className="text-sm text-textSubtle"
>
Back to model list
</button>
</div>
<Input
className="border-2 px-4 py-5"
placeholder="Type model name here"
onChange={(event) => setModel(event.target.value)}
value={model}
/>
{attemptedSubmit && validationErrors.model && (
<div className="text-red-500 text-sm mt-1">{validationErrors.model}</div>
)}
</div>
)}
</>
)}
</div>
)}
</div>
</Modal>
</div>
<DialogFooter className="pt-2">
<Button variant="outline" onClick={handleClose} type="button">
Cancel
</Button>
<Button onClick={handleSubmit} disabled={!isValid}>
Select model
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
};
@@ -4,13 +4,15 @@ import { useModelAndProvider } from '../../../ModelAndProviderContext';
import { Button } from '../../../ui/button';
import { Select } from '../../../ui/Select';
import { Input } from '../../../ui/input';
import { Info } from 'lucide-react';
import { getPredefinedModelsFromEnv, shouldShowPredefinedModels } from '../predefinedModelsUtils';
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '../../../ui/dialog';
interface LeadWorkerSettingsProps {
isOpen: boolean;
onClose: () => void;
}
export function LeadWorkerSettings({ onClose }: LeadWorkerSettingsProps) {
export function LeadWorkerSettings({ isOpen, onClose }: LeadWorkerSettingsProps) {
const { read, upsert, getProviders, remove } = useConfig();
const { currentModel } = useModelAndProvider();
const [leadModel, setLeadModel] = useState<string>('');
@@ -28,6 +30,8 @@ export function LeadWorkerSettings({ onClose }: LeadWorkerSettingsProps) {
// Load current configuration
useEffect(() => {
if (!isOpen) return; // Only load when modal is open
const loadConfig = async () => {
try {
setIsLoading(true);
@@ -48,11 +52,18 @@ export function LeadWorkerSettings({ onClose }: LeadWorkerSettingsProps) {
if (leadModelConfig) {
setLeadModel(leadModelConfig as string);
setIsEnabled(true);
} else {
setLeadModel('');
setIsEnabled(false);
}
if (leadProviderConfig) setLeadProvider(leadProviderConfig as string);
else setLeadProvider('');
if (leadTurnsConfig) setLeadTurns(Number(leadTurnsConfig));
else setLeadTurns(3);
if (failureThresholdConfig) setFailureThreshold(Number(failureThresholdConfig));
else setFailureThreshold(2);
if (fallbackTurnsConfig) setFallbackTurns(Number(fallbackTurnsConfig));
else setFallbackTurns(2);
// Set worker model to current model or from config
const workerModelConfig = await read('GOOSE_MODEL', false);
@@ -60,29 +71,47 @@ export function LeadWorkerSettings({ onClose }: LeadWorkerSettingsProps) {
setWorkerModel(workerModelConfig as string);
} else if (currentModel) {
setWorkerModel(currentModel as string);
} else {
setWorkerModel('');
}
const workerProviderConfig = await read('GOOSE_PROVIDER', false);
if (workerProviderConfig) {
setWorkerProvider(workerProviderConfig as string);
} else {
setWorkerProvider('');
}
// Load available models
const providers = await getProviders(false);
const activeProviders = providers.filter((p) => p.is_configured);
const options: { value: string; label: string; provider: string }[] = [];
activeProviders.forEach(({ metadata, name }) => {
if (metadata.known_models) {
metadata.known_models.forEach((model) => {
options.push({
value: model.name,
label: `${model.name} (${metadata.display_name})`,
provider: name,
});
if (shouldShowPredefinedModels()) {
// Use predefined models if available
const predefinedModels = getPredefinedModelsFromEnv();
predefinedModels.forEach((model) => {
options.push({
value: model.name, // Use name for switching
label: model.alias || model.name, // Use alias for display, fallback to name
provider: model.provider,
});
}
});
});
} else {
// Fallback to provider-based models
const providers = await getProviders(false);
const activeProviders = providers.filter((p) => p.is_configured);
activeProviders.forEach(({ metadata, name }) => {
if (metadata.known_models) {
metadata.known_models.forEach((model) => {
options.push({
value: model.name,
label: `${model.name} (${metadata.display_name})`,
provider: name,
});
});
}
});
}
setModelOptions(options);
} catch (error) {
@@ -93,7 +122,7 @@ export function LeadWorkerSettings({ onClose }: LeadWorkerSettingsProps) {
};
loadConfig();
}, [read, getProviders, currentModel]);
}, [read, getProviders, currentModel, isOpen]);
const handleSave = async () => {
try {
@@ -125,136 +154,166 @@ export function LeadWorkerSettings({ onClose }: LeadWorkerSettingsProps) {
};
if (isLoading) {
return <div className="p-4">Loading...</div>;
return (
<Dialog open={isOpen} onOpenChange={(open) => !open && onClose()}>
<DialogContent className="sm:max-w-[500px]">
<DialogHeader>
<DialogTitle>Lead/Worker Mode</DialogTitle>
</DialogHeader>
<div className="p-4">Loading...</div>
</DialogContent>
</Dialog>
);
}
return (
<div className="p-4 space-y-4">
<div className="space-y-2">
<h3 className="text-lg font-medium text-textProminent">Lead/Worker Mode</h3>
<p className="text-sm text-textSubtle">
Configure a lead model for planning and a worker model for execution
</p>
</div>
<div className="flex items-center space-x-2">
<input
type="checkbox"
id="enable-lead-worker"
checked={isEnabled}
onChange={(e) => setIsEnabled(e.target.checked)}
className="rounded border-borderStandard"
/>
<label htmlFor="enable-lead-worker" className="text-sm text-textStandard">
Enable lead/worker mode
</label>
</div>
<div className="space-y-4">
<div className="space-y-2">
<label className="text-sm text-textSubtle">Lead Model</label>
<Select
options={modelOptions}
value={modelOptions.find((opt) => opt.value === leadModel) || null}
onChange={(newValue: unknown) => {
const option = newValue as { value: string; provider: string } | null;
if (option) {
setLeadModel(option.value);
setLeadProvider(option.provider);
}
}}
placeholder="Select lead model..."
isDisabled={!isEnabled}
/>
<p className="text-xs text-textSubtle">
Strong model for initial planning and fallback recovery
</p>
</div>
<div className="space-y-2">
<label className="text-sm text-textSubtle">Worker Model</label>
<Select
options={modelOptions}
value={modelOptions.find((opt) => opt.value === workerModel) || null}
onChange={(newValue: unknown) => {
const option = newValue as { value: string; provider: string } | null;
if (option) {
setWorkerModel(option.value);
setWorkerProvider(option.provider);
}
}}
placeholder="Select worker model..."
isDisabled={!isEnabled}
/>
<p className="text-xs text-textSubtle">Fast model for routine execution tasks</p>
</div>
<div className="space-y-4 pt-4 border-t border-borderSubtle">
<Dialog open={isOpen} onOpenChange={(open) => !open && onClose()}>
<DialogContent className="sm:max-w-[500px]">
<DialogHeader>
<DialogTitle>Lead/Worker Mode</DialogTitle>
</DialogHeader>
<div className="p-4 space-y-4">
<div className="space-y-2">
<label className="text-sm text-textSubtle flex items-center gap-1">
Initial Lead Turns
<Info size={14} className="text-textSubtle" />
</label>
<Input
type="number"
min={1}
max={10}
value={leadTurns}
onChange={(e) => setLeadTurns(Number(e.target.value))}
className="w-20"
disabled={!isEnabled}
/>
<p className="text-xs text-textSubtle">
Number of turns to use the lead model at the start
<p className="text-sm text-textSubtle">
Configure a lead model for planning and a worker model for execution
</p>
</div>
<div className="space-y-2">
<label className="text-sm text-textSubtle flex items-center gap-1">
Failure Threshold
<Info size={14} className="text-textSubtle" />
</label>
<Input
type="number"
min={1}
max={5}
value={failureThreshold}
onChange={(e) => setFailureThreshold(Number(e.target.value))}
className="w-20"
disabled={!isEnabled}
<div className="flex items-center space-x-2">
<input
type="checkbox"
id="enable-lead-worker"
checked={isEnabled}
onChange={(e) => setIsEnabled(e.target.checked)}
className="rounded border-borderStandard"
/>
<p className="text-xs text-textSubtle">
Consecutive failures before switching back to lead
</p>
<label htmlFor="enable-lead-worker" className="text-sm text-textStandard">
Enable lead/worker mode
</label>
</div>
<div className="space-y-2">
<label className="text-sm text-textSubtle flex items-center gap-1">
Fallback Turns
<Info size={14} className="text-textSubtle" />
</label>
<Input
type="number"
min={1}
max={5}
value={fallbackTurns}
onChange={(e) => setFallbackTurns(Number(e.target.value))}
className="w-20"
disabled={!isEnabled}
/>
<p className="text-xs text-textSubtle">Turns to use lead model during fallback</p>
<div className="space-y-4">
<div className="space-y-2">
<label className={`text-sm ${!isEnabled ? 'text-text-muted' : 'text-textSubtle'}`}>
Lead Model
</label>
<Select
options={modelOptions}
value={modelOptions.find((opt) => opt.value === leadModel) || null}
onChange={(newValue: unknown) => {
const option = newValue as { value: string; provider: string } | null;
if (option) {
setLeadModel(option.value);
setLeadProvider(option.provider);
}
}}
placeholder="Select lead model..."
isDisabled={!isEnabled}
className={!isEnabled ? 'opacity-50' : ''}
/>
<p className={`text-xs ${!isEnabled ? 'text-text-muted' : 'text-textSubtle'}`}>
Strong model for initial planning and fallback recovery
</p>
</div>
<div className="space-y-2">
<label className={`text-sm ${!isEnabled ? 'text-text-muted' : 'text-textSubtle'}`}>
Worker Model
</label>
<Select
options={modelOptions}
value={modelOptions.find((opt) => opt.value === workerModel) || null}
onChange={(newValue: unknown) => {
const option = newValue as { value: string; provider: string } | null;
if (option) {
setWorkerModel(option.value);
setWorkerProvider(option.provider);
}
}}
placeholder="Select worker model..."
isDisabled={!isEnabled}
className={!isEnabled ? 'opacity-50' : ''}
/>
<p className={`text-xs ${!isEnabled ? 'text-text-muted' : 'text-textSubtle'}`}>
Fast model for routine execution tasks
</p>
</div>
<div
className={`space-y-4 pt-4 border-t border-borderSubtle ${!isEnabled ? 'opacity-50' : ''}`}
>
<div className="space-y-2">
<label
className={`text-sm flex items-center gap-1 ${!isEnabled ? 'text-text-muted' : 'text-textSubtle'}`}
>
Initial Lead Turns
</label>
<Input
type="number"
min={1}
max={10}
value={leadTurns}
onChange={(e) => setLeadTurns(Number(e.target.value))}
className={`w-20 ${!isEnabled ? 'opacity-50 cursor-not-allowed' : ''}`}
disabled={!isEnabled}
/>
<p className={`text-xs ${!isEnabled ? 'text-text-muted' : 'text-textSubtle'}`}>
Number of turns to use the lead model at the start
</p>
</div>
<div className="space-y-2">
<label
className={`text-sm flex items-center gap-1 ${!isEnabled ? 'text-text-muted' : 'text-textSubtle'}`}
>
Failure Threshold
</label>
<Input
type="number"
min={1}
max={5}
value={failureThreshold}
onChange={(e) => setFailureThreshold(Number(e.target.value))}
className={`w-20 ${!isEnabled ? 'opacity-50 cursor-not-allowed' : ''}`}
disabled={!isEnabled}
/>
<p className={`text-xs ${!isEnabled ? 'text-text-muted' : 'text-textSubtle'}`}>
Consecutive failures before switching back to lead
</p>
</div>
<div className="space-y-2">
<label
className={`text-sm flex items-center gap-1 ${!isEnabled ? 'text-text-muted' : 'text-textSubtle'}`}
>
Fallback Turns
</label>
<Input
type="number"
min={1}
max={5}
value={fallbackTurns}
onChange={(e) => setFallbackTurns(Number(e.target.value))}
className={`w-20 ${!isEnabled ? 'opacity-50 cursor-not-allowed' : ''}`}
disabled={!isEnabled}
/>
<p className={`text-xs ${!isEnabled ? 'text-text-muted' : 'text-textSubtle'}`}>
Turns to use lead model during fallback
</p>
</div>
</div>
</div>
<div className="flex justify-end space-x-2 pt-4 border-t border-borderSubtle">
<Button variant="ghost" onClick={onClose}>
Cancel
</Button>
<Button onClick={handleSave} disabled={isEnabled && (!leadModel || !workerModel)}>
Save Settings
</Button>
</div>
</div>
</div>
<div className="flex justify-end space-x-2 pt-4 border-t border-borderSubtle">
<Button variant="ghost" onClick={onClose}>
Cancel
</Button>
<Button onClick={handleSave} disabled={isEnabled && (!leadModel || !workerModel)}>
Save Settings
</Button>
</div>
</div>
</DialogContent>
</Dialog>
);
}
@@ -1,25 +1,42 @@
import { AddModelButton } from './AddModelButton';
import { useState } from 'react';
import { Button } from '../../../ui/button';
import { Sliders } from 'lucide-react';
import { AddModelModal } from './AddModelModal';
import type { View } from '../../../../App';
import { shouldShowPredefinedModels } from '../predefinedModelsUtils';
interface ConfigureModelButtonsProps {
setView: (view: View) => void;
}
export default function ModelSettingsButtons({ setView }: ConfigureModelButtonsProps) {
const [isAddModelModalOpen, setIsAddModelModalOpen] = useState(false);
const hasPredefinedModels = shouldShowPredefinedModels();
return (
<div className="flex gap-4 pt-4 ">
<AddModelButton setView={setView} />
<div className="flex gap-2 pt-4">
<Button
className="flex items-center gap-2 justify-center text-textStandard bg-bgApp border border-borderSubtle hover:border-borderProminent hover:bg-bgApp [&>svg]:!size-4"
onClick={() => {
setView('ConfigureProviders');
}}
className="flex items-center gap-2 justify-center"
variant="default"
size="sm"
onClick={() => setIsAddModelModalOpen(true)}
>
<Sliders className="rotate-90" />
Configure providers
Switch models
</Button>
{isAddModelModalOpen ? (
<AddModelModal setView={setView} onClose={() => setIsAddModelModalOpen(false)} />
) : null}
{!hasPredefinedModels && (
<Button
className="flex items-center gap-2 justify-center"
variant="secondary"
size="sm"
onClick={() => {
setView('ConfigureProviders');
}}
>
Configure providers
</Button>
)}
</div>
);
}