Просмотр исходного кода

feat: disable settings while task is running (#1291)

* feat: disable settings while task is running

* test files

* tests

* test
Alex Bäuerle 1 год назад
Родитель
Сommit
620bbb38cf

+ 6 - 2
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");
 });

+ 32 - 16
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 (
-    <Autocomplete
-      aria-label={ariaLabel}
-      label={t(LABELS[ariaLabel])}
-      placeholder={t(PLACEHOLDERS[ariaLabel])}
-      defaultItems={items}
-      defaultSelectedKey={defaultKey}
-      allowsCustomValue={allowCustomValue}
-      onInputChange={(value) => {
-        onChange(value);
-      }}
+    <Tooltip
+      content={
+        disabled
+          ? `${tooltip} ${t(I18nKey.SETTINGS$DISABLED_RUNNING)}`
+          : tooltip
+      }
+      closeDelay={100}
+      delay={500}
     >
-      {(item) => (
-        <AutocompleteItem key={item.value}>{item.label}</AutocompleteItem>
-      )}
-    </Autocomplete>
+      <Autocomplete
+        aria-label={ariaLabel}
+        label={t(LABELS[ariaLabel])}
+        placeholder={t(PLACEHOLDERS[ariaLabel])}
+        defaultItems={items}
+        defaultSelectedKey={defaultKey}
+        isDisabled={disabled}
+        allowsCustomValue={allowCustomValue}
+        onInputChange={(value) => {
+          onChange(value);
+        }}
+      >
+        {(item) => (
+          <AutocompleteItem key={item.value}>{item.label}</AutocompleteItem>
+        )}
+      </Autocomplete>
+    </Tooltip>
   );
 }
 
 AutocompleteCombobox.defaultProps = {
   allowCustomValue: false,
+  disabled: false,
 };

+ 26 - 3
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<Settings>) => {
-  render(
+  renderWithProviders(
     <SettingsForm
       settings={settings}
       models={["model1", "model2", "model3"]}
@@ -49,6 +51,27 @@ describe("SettingsForm", () => {
     expect(languageInput).toHaveValue("Español");
   });
 
+  it("should disable settings while task is running", () => {
+    renderWithProviders(
+      <SettingsForm
+        settings={{}}
+        models={["model1", "model2", "model3"]}
+        agents={["agent1", "agent2", "agent3"]}
+        onModelChange={onModelChangeMock}
+        onAgentChange={onAgentChangeMock}
+        onLanguageChange={onLanguageChangeMock}
+      />,
+      { 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({});

+ 28 - 2
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<boolean>(false);
+
+  useEffect(() => {
+    if (
+      curTaskState === AgentTaskState.RUNNING ||
+      curTaskState === AgentTaskState.PAUSED
+    ) {
+      setDisabled(true);
+    } else {
+      setDisabled(false);
+    }
+  }, [curTaskState, setDisabled]);
+
   return (
     <>
       <AutocompleteCombobox
@@ -27,19 +47,25 @@ function SettingsForm({
         items={models.map((model) => ({ 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}
       />
       <AutocompleteCombobox
         ariaLabel="agent"
         items={agents.map((agent) => ({ value: agent, label: agent }))}
         defaultKey={settings.AGENT || agents[0]}
         onChange={onAgentChange}
+        tooltip={t(I18nKey.SETTINGS$AGENT_TOOLTIP)}
+        disabled={disabled}
       />
       <AutocompleteCombobox
         ariaLabel="language"
         items={AvailableLanguages}
         defaultKey={settings.LANGUAGE || "en"}
         onChange={onLanguageChange}
+        tooltip={t(I18nKey.SETTINGS$LANGUAGE_TOOLTIP)}
+        disabled={disabled}
       />
     </>
   );

+ 14 - 11
frontend/src/components/modals/settings/SettingsModal.test.tsx

@@ -1,13 +1,14 @@
-import { waitFor, screen, act, render } from "@testing-library/react";
-import userEvent from "@testing-library/user-event";
-import React from "react";
-import { Mock } from "vitest";
 import {
-  fetchModels,
   fetchAgents,
-  saveSettings,
+  fetchModels,
   getCurrentSettings,
-} from "../../../services/settingsService";
+  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 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(<SettingsModal isOpen onOpenChange={vi.fn()} />);
+    renderWithProviders(<SettingsModal isOpen onOpenChange={vi.fn()} />);
 
     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(<SettingsModal isOpen onOpenChange={onOpenChange} />),
+      renderWithProviders(<SettingsModal isOpen onOpenChange={onOpenChange} />),
     );
 
     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(<SettingsModal isOpen onOpenChange={onOpenChangeMock} />),
+      renderWithProviders(
+        <SettingsModal isOpen onOpenChange={onOpenChangeMock} />,
+      ),
     );
 
     const saveButton = screen.getByRole("button", { name: /save/i });
@@ -97,7 +100,7 @@ describe("SettingsModal", () => {
     });
 
     const onOpenChange = vi.fn();
-    const { rerender } = render(
+    const { rerender } = renderWithProviders(
       <SettingsModal isOpen onOpenChange={onOpenChange} />,
     );
 

+ 16 - 0
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."
   }
 }