feat(frontend): "Reset to Default" button (#1573)

* frontend: reset-button

* frontend: key prop removed, issue with uncontrolled Autocomplete input

* frontend: reset button test, Autocomplete switch to controlled input

* frontend: proper use of getDefaultSettings in test

* frontend: separate selectedKey and inputValue in Autocompletecombobox

* no fallbacks, defaultSelectedKey prop is used to prevent the input from clearing itself

* remove conflict resolution fragments

---------

Co-authored-by: sp.wack <83104063+amanape@users.noreply.github.com>
Co-authored-by: amanape <stephanpsaras@gmail.com>
This commit is contained in:
Lev 2024-05-07 10:17:08 -07:00 committed by GitHub
parent fda21d2ce3
commit 4a2a35b6cf
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 87 additions and 17 deletions

View File

@ -60,7 +60,6 @@ describe("AutocompleteCombobox", () => {
userEvent.click(model2);
});
expect(modelInput).toHaveValue("model2");
expect(onChangeMock).toHaveBeenCalledWith("model2");
});

View File

@ -58,16 +58,17 @@ export function AutocompleteCombobox({
label={t(LABELS[ariaLabel])}
placeholder={t(PLACEHOLDERS[ariaLabel])}
defaultItems={items}
defaultInputValue={
defaultSelectedKey={defaultKey}
inputValue={
// Find the label for the default key, otherwise use the default key itself
// This is useful when the default key is not in the list of items, in the case of a custom LLM model
items.find((item) => item.value === defaultKey)?.label || defaultKey
}
onInputChange={(val) => {
onChange(val);
}}
isDisabled={disabled}
allowsCustomValue={allowCustomValue}
onInputChange={(value) => {
onChange(value);
}}
>
{(item) => (
<AutocompleteItem key={item.value}>{item.label}</AutocompleteItem>

View File

@ -37,7 +37,7 @@ function SettingsForm({
<AutocompleteCombobox
ariaLabel="agent"
items={agents.map((agent) => ({ value: agent, label: agent }))}
defaultKey={settings.AGENT || agents[0]}
defaultKey={settings.AGENT}
onChange={onAgentChange}
tooltip={t(I18nKey.SETTINGS$AGENT_TOOLTIP)}
disabled={disabled}
@ -45,7 +45,7 @@ function SettingsForm({
<AutocompleteCombobox
ariaLabel="model"
items={models.map((model) => ({ value: model, label: model }))}
defaultKey={settings.LLM_MODEL || models[0]}
defaultKey={settings.LLM_MODEL}
onChange={(e) => {
onModelChange(e);
}}
@ -81,7 +81,7 @@ function SettingsForm({
<AutocompleteCombobox
ariaLabel="language"
items={AvailableLanguages}
defaultKey={settings.LANGUAGE || "en"}
defaultKey={settings.LANGUAGE}
onChange={onLanguageChange}
tooltip={t(I18nKey.SETTINGS$LANGUAGE_TOOLTIP)}
disabled={disabled}

View File

@ -5,7 +5,12 @@ import React from "react";
import { renderWithProviders } from "test-utils";
import { Mock } from "vitest";
import toast from "#/utils/toast";
import { Settings, getSettings, saveSettings } from "#/services/settings";
import {
Settings,
getSettings,
saveSettings,
getDefaultSettings,
} from "#/services/settings";
import { initializeAgent } from "#/services/agent";
import { fetchAgents, fetchModels } from "#/api";
import SettingsModal from "./SettingsModal";
@ -20,6 +25,12 @@ vi.mock("#/services/settings", async (importOriginal) => ({
AGENT: "MonologueAgent",
LANGUAGE: "en",
}),
getDefaultSettings: vi.fn().mockReturnValue({
LLM_MODEL: "gpt-3.5-turbo",
AGENT: "CodeActAgent",
LANGUAGE: "en",
LLM_API_KEY: "",
}),
settingsAreUpToDate: vi.fn().mockReturnValue(true),
saveSettings: vi.fn(),
}));
@ -52,7 +63,7 @@ describe("SettingsModal", () => {
});
});
it("should close the modal when the cancel button is clicked", async () => {
it("should close the modal when the close button is clicked", async () => {
const onOpenChange = vi.fn();
await act(async () =>
renderWithProviders(<SettingsModal isOpen onOpenChange={onOpenChange} />),
@ -237,7 +248,35 @@ describe("SettingsModal", () => {
});
});
it.todo("should reset setting changes when the cancel button is clicked");
it("should reset settings to defaults when the 'reset to defaults' button is clicked", async () => {
const onOpenChangeMock = vi.fn();
await act(async () =>
renderWithProviders(
<SettingsModal isOpen onOpenChange={onOpenChangeMock} />,
),
);
const resetButton = screen.getByRole("button", {
name: /MODAL_RESET_BUTTON_LABEL/i,
});
const agentInput = screen.getByRole("combobox", { name: "agent" });
act(() => {
userEvent.click(agentInput);
});
const agent3 = screen.getByText("agent3");
act(() => {
userEvent.click(agent3);
});
expect(agentInput).toHaveValue("agent3");
act(() => {
userEvent.click(resetButton);
});
expect(getDefaultSettings).toHaveBeenCalled();
expect(agentInput).toHaveValue("CodeActAgent"); // Agent value is reset to default from getDefaultSettings()
});
it.todo(
"should display a loading spinner when fetching the models and agents",

View File

@ -12,6 +12,7 @@ import AgentState from "../../../types/AgentState";
import {
Settings,
getSettings,
getDefaultSettings,
getSettingsDifference,
settingsAreUpToDate,
maybeMigrateSettings,
@ -79,17 +80,22 @@ function SettingsModal({ isOpen, onOpenChange }: SettingsProps) {
};
const handleLanguageChange = (language: string) => {
const key = AvailableLanguages.find(
(lang) => lang.label === language,
)?.value;
if (key) setSettings((prev) => ({ ...prev, LANGUAGE: key }));
const key =
AvailableLanguages.find((lang) => lang.label === language)?.value ||
language;
// The appropriate key is assigned when the user selects a language.
// Otherwise, their input is reflected in the inputValue field of the Autocomplete component.
setSettings((prev) => ({ ...prev, LANGUAGE: key }));
};
const handleAPIKeyChange = (key: string) => {
setSettings((prev) => ({ ...prev, LLM_API_KEY: key }));
};
const handleResetSettings = () => {
setSettings(getDefaultSettings);
};
const handleSaveSettings = () => {
const updatedSettings = getSettingsDifference(settings);
saveSettings(settings);
@ -139,6 +145,12 @@ function SettingsModal({ isOpen, onOpenChange }: SettingsProps) {
closeAfterAction: true,
className: "bg-primary rounded-lg",
},
{
label: t(I18nKey.CONFIGURATION$MODAL_RESET_BUTTON_LABEL),
action: handleResetSettings,
closeAfterAction: false,
className: "bg-neutral-500 rounded-lg",
},
{
label: t(I18nKey.CONFIGURATION$MODAL_CLOSE_BUTTON_LABEL),
action: () => {
@ -146,7 +158,7 @@ function SettingsModal({ isOpen, onOpenChange }: SettingsProps) {
},
isDisabled: !settingsAreUpToDate(),
closeAfterAction: true,
className: "bg-neutral-500 rounded-lg",
className: "bg-rose-600 rounded-lg",
},
]}
>

View File

@ -242,6 +242,20 @@
"pt": "Salvar",
"es": "Guardar"
},
"CONFIGURATION$MODAL_RESET_BUTTON_LABEL": {
"en": "Reset to Defaults",
"zh-CN": "重置为默认值",
"de": "Auf Standardwerte zurücksetzen",
"ko-KR": "기본값으로 재설정",
"no": "Tilbakestill til standardverdier",
"zh-TW": "重設為預設值",
"it": "Reimposta ai valori predefiniti",
"pt": "Redefinir para os padrões",
"es": "Restablecer valores predeterminados",
"ar": "إعادة التعيين إلى الإعدادات الافتراضية",
"fr": "Réinitialiser aux valeurs par défaut",
"tr": "Varsayılanlara Sıfırla"
},
"CONFIGURATION$SETTINGS_NEED_UPDATE_MESSAGE": {
"en": "We've changed some settings in the latest update. Take a minute to review."
},

View File

@ -38,6 +38,11 @@ export const maybeMigrateSettings = () => {
}
};
/**
* Get the default settings
*/
export const getDefaultSettings = (): Settings => DEFAULT_SETTINGS;
/**
* Get the settings from local storage or use the default settings if not found
*/