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:
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user