diff --git a/frontend/src/components/modals/settings/AutocompleteCombobox.test.tsx b/frontend/src/components/modals/settings/AutocompleteCombobox.test.tsx index e8002ac42a..108ac25871 100644 --- a/frontend/src/components/modals/settings/AutocompleteCombobox.test.tsx +++ b/frontend/src/components/modals/settings/AutocompleteCombobox.test.tsx @@ -1,6 +1,7 @@ -import { render, screen, act } from "@testing-library/react"; -import React from "react"; +import { act, render, screen } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; +import React from "react"; +import { vi } from "vitest"; import { AutocompleteCombobox } from "./AutocompleteCombobox"; const onChangeMock = vi.fn(); @@ -15,6 +16,7 @@ const renderComponent = () => { value: "m3", label: "model3" }, ]} defaultKey="m1" + tooltip="tooltip" onChange={onChangeMock} />, ); @@ -61,4 +63,6 @@ describe("AutocompleteCombobox", () => { expect(modelInput).toHaveValue("model2"); expect(onChangeMock).toHaveBeenCalledWith("model2"); }); + + it.todo("should show a tooltip after 0.5 seconds of focus"); }); diff --git a/frontend/src/components/modals/settings/AutocompleteCombobox.tsx b/frontend/src/components/modals/settings/AutocompleteCombobox.tsx index 353ea1d407..c4cdc7af17 100644 --- a/frontend/src/components/modals/settings/AutocompleteCombobox.tsx +++ b/frontend/src/components/modals/settings/AutocompleteCombobox.tsx @@ -1,7 +1,7 @@ -import { Autocomplete, AutocompleteItem } from "@nextui-org/react"; +import { I18nKey } from "#/i18n/declaration"; +import { Autocomplete, AutocompleteItem, Tooltip } from "@nextui-org/react"; import React from "react"; import { useTranslation } from "react-i18next"; -import { I18nKey } from "#/i18n/declaration"; type Label = "model" | "agent" | "language"; @@ -27,7 +27,9 @@ interface AutocompleteComboboxProps { items: AutocompleteItemType[]; defaultKey: string; onChange: (key: string) => void; + tooltip: string; allowCustomValue?: boolean; + disabled?: boolean; } export function AutocompleteCombobox({ @@ -35,29 +37,43 @@ export function AutocompleteCombobox({ items, defaultKey, onChange, + tooltip, allowCustomValue = false, + disabled = false, }: AutocompleteComboboxProps) { const { t } = useTranslation(); return ( - { - onChange(value); - }} + - {(item) => ( - {item.label} - )} - + { + onChange(value); + }} + > + {(item) => ( + {item.label} + )} + + ); } AutocompleteCombobox.defaultProps = { allowCustomValue: false, + disabled: false, }; diff --git a/frontend/src/components/modals/settings/SettingsForm.test.tsx b/frontend/src/components/modals/settings/SettingsForm.test.tsx index 212d6aa35b..a144c06426 100644 --- a/frontend/src/components/modals/settings/SettingsForm.test.tsx +++ b/frontend/src/components/modals/settings/SettingsForm.test.tsx @@ -1,6 +1,8 @@ -import React from "react"; -import { act, render, screen } from "@testing-library/react"; +import { act, screen } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; +import React from "react"; +import { renderWithProviders } from "test-utils"; +import AgentTaskState from "#/types/AgentTaskState"; import SettingsForm from "./SettingsForm"; const onModelChangeMock = vi.fn(); @@ -8,7 +10,7 @@ const onAgentChangeMock = vi.fn(); const onLanguageChangeMock = vi.fn(); const renderSettingsForm = (settings: Partial) => { - render( + renderWithProviders( { expect(languageInput).toHaveValue("Español"); }); + it("should disable settings while task is running", () => { + renderWithProviders( + , + { preloadedState: { agent: { curTaskState: AgentTaskState.RUNNING } } }, + ); + const modelInput = screen.getByRole("combobox", { name: "model" }); + const agentInput = screen.getByRole("combobox", { name: "agent" }); + const languageInput = screen.getByRole("combobox", { name: "language" }); + + expect(modelInput).toBeDisabled(); + expect(agentInput).toBeDisabled(); + expect(languageInput).toBeDisabled(); + }); + describe("onChange handlers", () => { it("should call the onModelChange handler when the model changes", () => { renderSettingsForm({}); diff --git a/frontend/src/components/modals/settings/SettingsForm.tsx b/frontend/src/components/modals/settings/SettingsForm.tsx index 71cbde9150..e5a82053d8 100644 --- a/frontend/src/components/modals/settings/SettingsForm.tsx +++ b/frontend/src/components/modals/settings/SettingsForm.tsx @@ -1,5 +1,10 @@ -import React from "react"; -import { AvailableLanguages } from "#/i18n"; +import React, { useEffect } from "react"; +import { useTranslation } from "react-i18next"; +import { useSelector } from "react-redux"; +import { AvailableLanguages } from "../../../i18n"; +import { I18nKey } from "../../../i18n/declaration"; +import { RootState } from "../../../store"; +import AgentTaskState from "../../../types/AgentTaskState"; import { AutocompleteCombobox } from "./AutocompleteCombobox"; interface SettingsFormProps { @@ -20,6 +25,21 @@ function SettingsForm({ onAgentChange, onLanguageChange, }: SettingsFormProps) { + const { t } = useTranslation(); + const { curTaskState } = useSelector((state: RootState) => state.agent); + const [disabled, setDisabled] = React.useState(false); + + useEffect(() => { + if ( + curTaskState === AgentTaskState.RUNNING || + curTaskState === AgentTaskState.PAUSED + ) { + setDisabled(true); + } else { + setDisabled(false); + } + }, [curTaskState, setDisabled]); + return ( <> ({ value: model, label: model }))} defaultKey={settings.LLM_MODEL || models[0]} onChange={onModelChange} + tooltip={t(I18nKey.SETTINGS$MODEL_TOOLTIP)} allowCustomValue // user can type in a custom LLM model that is not in the list + disabled={disabled} /> ({ value: agent, label: agent }))} defaultKey={settings.AGENT || agents[0]} onChange={onAgentChange} + tooltip={t(I18nKey.SETTINGS$AGENT_TOOLTIP)} + disabled={disabled} /> ); diff --git a/frontend/src/components/modals/settings/SettingsModal.test.tsx b/frontend/src/components/modals/settings/SettingsModal.test.tsx index b9f6a62d14..fdc91ecd59 100644 --- a/frontend/src/components/modals/settings/SettingsModal.test.tsx +++ b/frontend/src/components/modals/settings/SettingsModal.test.tsx @@ -1,13 +1,14 @@ -import { waitFor, screen, act, render } from "@testing-library/react"; +import { + fetchAgents, + fetchModels, + getCurrentSettings, + saveSettings, +} from "#/services/settingsService"; +import { act, screen, waitFor } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import React from "react"; +import { renderWithProviders } from "test-utils"; import { Mock } from "vitest"; -import { - fetchModels, - fetchAgents, - saveSettings, - getCurrentSettings, -} from "../../../services/settingsService"; import SettingsModal from "./SettingsModal"; vi.mock("../../services/settingsService", async (importOriginal) => ({ @@ -28,7 +29,7 @@ describe("SettingsModal", () => { }); it("should fetch existing agents and models from the API", async () => { - render(); + renderWithProviders(); await waitFor(() => { expect(fetchModels).toHaveBeenCalledTimes(1); @@ -43,7 +44,7 @@ describe("SettingsModal", () => { it("should close the modal when the cancel button is clicked", async () => { const onOpenChange = vi.fn(); await act(async () => - render(), + renderWithProviders(), ); const cancelButton = screen.getByRole("button", { @@ -60,7 +61,9 @@ describe("SettingsModal", () => { it("should call saveSettings (and close) with the new values", async () => { const onOpenChangeMock = vi.fn(); await act(async () => - render(), + renderWithProviders( + , + ), ); const saveButton = screen.getByRole("button", { name: /save/i }); @@ -97,7 +100,7 @@ describe("SettingsModal", () => { }); const onOpenChange = vi.fn(); - const { rerender } = render( + const { rerender } = renderWithProviders( , ); diff --git a/frontend/src/i18n/translation.json b/frontend/src/i18n/translation.json index 915f429b78..f794193636 100644 --- a/frontend/src/i18n/translation.json +++ b/frontend/src/i18n/translation.json @@ -308,5 +308,21 @@ "ar": "مساعد", "fr": "Assistant", "tr": "Gönder" + }, + "SETTINGS$MODEL_TOOLTIP": { + "en": "Select the language model to use.", + "de": "Wähle das zu verwendende Model." + }, + "SETTINGS$AGENT_TOOLTIP": { + "en": "Select the agent to use.", + "de": "Wähle den zu verwendenden Agent." + }, + "SETTINGS$LANGUAGE_TOOLTIP": { + "en": "Select the language for the UI.", + "de": "Wähle die Sprache für das Interface." + }, + "SETTINGS$DISABLED_RUNNING": { + "en": "Cannot be changed while the agent is running.", + "de": "Kann nicht geändert werden während ein Task ausgeführt wird." } }