Friction: Model Wizard: also warn if some keys are not saved

This commit is contained in:
Enrico Ros
2025-11-05 12:21:50 -08:00
parent 9446f15922
commit 1f30f1168f
2 changed files with 70 additions and 17 deletions
@@ -40,6 +40,7 @@ export function ModelsConfiguratorModal(props: {
// state
// const [showAllServices, setShowAllServices] = React.useState<boolean>(false);
const [tab, setTab] = React.useState<TabValue>(MODELS_WIZARD_ENABLE_INITIALLY && !modelsServices.length ? 'wizard' : 'setup');
const [unsavedWizardProviders, setUnsavedWizardProviders] = React.useState<Set<string>>(new Set());
const showAllServices = false;
// external state
@@ -83,6 +84,17 @@ export function ModelsConfiguratorModal(props: {
const handleShowWizard = React.useCallback(() => setTab('wizard'), []);
// const handleToggleDefaults = React.useCallback(() => setTab(tab => tab === 'defaults' ? 'setup' : 'defaults'), []);
// callback for wizard to report unsaved provider changes
const handleWizardProviderUnsavedChange = React.useCallback((providerId: string, hasUnsaved: boolean) => {
setUnsavedWizardProviders(prev => {
const next = new Set(prev);
if (hasUnsaved) next.add(providerId);
else next.delete(providerId);
// only update if actually changed
return next.size !== prev.size || (hasUnsaved && !prev.has(providerId)) ? next : prev;
});
}, []);
// start button
const startButton = React.useMemo(() => {
@@ -117,25 +129,35 @@ export function ModelsConfiguratorModal(props: {
const wizardButtons = React.useMemo(() => {
if (!isTabWizard) return undefined;
const hasUnsavedChanges = unsavedWizardProviders.size > 0;
// const tooltipTitle = !hasLLMs ? 'Please save at least one API key to continue'
// : hasUnsavedChanges ? 'You have unsaved changes - click Save first'
// : '';
return (
<Box sx={{ display: 'flex', width: '100%', gap: 1, justifyContent: 'space-between' }}>
<Box sx={{ display: 'flex', width: '100%', gap: 1, justifyContent: 'space-between', alignItems: 'center' }}>
{startButton}
<TooltipOutlined
title={!hasLLMs ? 'Please save at least one API key to continue' : ''}
placement='top'
{/* unsaved warning */}
{hasUnsavedChanges && (
<Typography color='warning' level='body-sm' ml='auto'>
{isMobile ? 'Unsaved' : `You have ${unsavedWizardProviders.size} unsaved change${ unsavedWizardProviders.size > 1 ? 's' : '' }`}
</Typography>
)}
{/* "Done" button */}
<Button
variant='solid'
color='neutral'
disabled={!hasLLMs || hasUnsavedChanges}
onClick={optimaActions().closeModels}
sx={{ ml: 'auto', minWidth: 100 }}
>
<Button
color='neutral'
disabled={!hasLLMs}
onClick={optimaActions().closeModels}
sx={{ ml: 'auto', minWidth: 100 }}
>
Close
</Button>
</TooltipOutlined>
Done
</Button>
</Box>
);
}, [isTabWizard, startButton, hasLLMs]);
}, [hasLLMs, unsavedWizardProviders, isMobile, isTabWizard, startButton]);
// Explainer section
@@ -239,7 +261,15 @@ export function ModelsConfiguratorModal(props: {
>
{isTabWizard && <Divider />}
{isTabWizard && <ModelsWizard isMobile={isMobile} onSkip={optimaActions().closeModels} onSwitchToAdvanced={handleShowAdvanced} onSwitchToWhy={handleShowExplainerAgain} />}
{isTabWizard && (
<ModelsWizard
isMobile={isMobile}
onSkip={optimaActions().closeModels}
onSwitchToAdvanced={handleShowAdvanced}
onSwitchToWhy={handleShowExplainerAgain}
onProviderUnsavedChange={handleWizardProviderUnsavedChange}
/>
)}
{isTabSetup && <ModelsServiceSelector modelsServices={modelsServices} selectedServiceId={activeServiceId} setSelectedServiceId={setConfServiceId} onSwitchToWizard={handleShowWizard} />}
{isTabSetup && <Divider sx={activeService ? undefined : { visibility: 'hidden' }} />}
+25 -2
View File
@@ -90,6 +90,7 @@ function WizardProviderSetup(props: {
provider: WizardProvider,
isFirst: boolean,
isHidden: boolean,
onUnsavedChange: (providerId: string, hasUnsaved: boolean) => void,
}) {
const { cat: providerCat, vendor: providerVendor, settingsKey: providerSettingsKey, omit: providerOmit } = props.provider;
@@ -134,6 +135,22 @@ function WizardProviderSetup(props: {
const autoCompleteId = isLocal ? `${providerVendor.id}-host` : `${providerVendor.id}-key`;
// wrapped setter that notifies parent of unsaved state
const { onUnsavedChange } = props;
const handleLocalValueChange = React.useCallback((newValue: string) => {
// set locally
setLocalValue(newValue);
// notify parent of unsaved state
if (providerOmit || !onUnsavedChange) return;
const hasUnsaved = newValue !== (serviceKeyValue || '');
const hasValue = !!newValue.trim();
onUnsavedChange(providerVendor.id, hasUnsaved && hasValue);
}, [onUnsavedChange, providerOmit, providerVendor.id, serviceKeyValue]);
// handlers
@@ -149,6 +166,10 @@ function WizardProviderSetup(props: {
const newKey = localValue?.trim() ?? '';
updateServiceSettings(vendorServiceId, { [providerSettingsKey]: newKey });
// notify parent that changes are now saved
if (!providerOmit)
onUnsavedChange(providerVendor.id, false);
// if the key is empty, remove the models
if (!newKey) {
setUpdateError(null);
@@ -170,7 +191,7 @@ function WizardProviderSetup(props: {
}
setIsLoading(false);
}, [localValue, providerSettingsKey, providerVendor, valueName]);
}, [localValue, onUnsavedChange, providerOmit, providerSettingsKey, providerVendor, valueName]);
// memoed components
@@ -232,7 +253,7 @@ function WizardProviderSetup(props: {
autoCompleteId={autoCompleteId}
value={localValue ?? ''}
placeholder={`${vendorName} ${valueName}`}
onChange={setLocalValue}
onChange={handleLocalValueChange}
required={false}
/>
</Box>
@@ -262,6 +283,7 @@ export function ModelsWizard(props: {
onSkip?: () => void,
onSwitchToAdvanced?: () => void,
onSwitchToWhy?: () => void,
onProviderUnsavedChange: (providerId: string, hasUnsaved: boolean) => void,
}) {
// state
@@ -296,6 +318,7 @@ export function ModelsWizard(props: {
provider={provider}
isFirst={!index}
isHidden={provider.cat !== activeCategory}
onUnsavedChange={props.onProviderUnsavedChange}
/>
))}