diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 63e3865214..339268e92d 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -9,13 +9,14 @@ import Workspace from "#/components/Workspace"; import LoadPreviousSessionModal from "#/components/modals/load-previous-session/LoadPreviousSessionModal"; import SettingsModal from "#/components/modals/settings/SettingsModal"; import { fetchMsgTotal } from "#/services/session"; -import { initializeAgent } from "#/services/settingsService"; import Socket from "#/services/socket"; import { ResFetchMsgTotal } from "#/types/ResponseType"; import "./App.css"; import AgentControlBar from "./components/AgentControlBar"; import AgentStatusBar from "./components/AgentStatusBar"; import Terminal from "./components/terminal/Terminal"; +import { initializeAgent } from "./services/agent"; +import { getSettings } from "./services/settings"; interface Props { setSettingOpen: (isOpen: boolean) => void; @@ -72,7 +73,7 @@ function App(): JSX.Element { if (initOnce) return; initOnce = true; - initializeAgent(); + initializeAgent(getSettings()); Socket.registerCallback("open", [getMsgTotal]); diff --git a/frontend/src/api/index.ts b/frontend/src/api/index.ts new file mode 100644 index 0000000000..6c0f0d840c --- /dev/null +++ b/frontend/src/api/index.ts @@ -0,0 +1,9 @@ +export async function fetchModels() { + const response = await fetch(`/api/litellm-models`); + return response.json(); +} + +export async function fetchAgents() { + const response = await fetch(`/api/agents`); + return response.json(); +} diff --git a/frontend/src/components/modals/base-modal/BaseModal.test.tsx b/frontend/src/components/modals/base-modal/BaseModal.test.tsx index c0041299cb..00939ae4ab 100644 --- a/frontend/src/components/modals/base-modal/BaseModal.test.tsx +++ b/frontend/src/components/modals/base-modal/BaseModal.test.tsx @@ -96,6 +96,42 @@ describe("BaseModal", () => { expect(screen.getByText("Children")).toBeInTheDocument(); }); + it("should disable the action given the condition", () => { + const { rerender } = render( + {}, + isDisabled: true, + }, + ]} + />, + ); + + expect(screen.getByText("Save")).toBeDisabled(); + + rerender( + {}, + isDisabled: false, + }, + ]} + />, + ); + + expect(screen.getByText("Save")).not.toBeDisabled(); + }); + it.skip("should not close if the backdrop or escape key is pressed", () => { const onOpenChangeMock = vi.fn(); render( diff --git a/frontend/src/components/modals/base-modal/FooterContent.tsx b/frontend/src/components/modals/base-modal/FooterContent.tsx index 8115ae3f3f..64a2c2ae28 100644 --- a/frontend/src/components/modals/base-modal/FooterContent.tsx +++ b/frontend/src/components/modals/base-modal/FooterContent.tsx @@ -3,6 +3,7 @@ import React from "react"; export interface Action { action: () => void; + isDisabled?: boolean; label: string; className?: string; closeAfterAction?: boolean; @@ -16,19 +17,22 @@ interface FooterContentProps { export function FooterContent({ actions, closeModal }: FooterContentProps) { return ( <> - {actions.map(({ action, label, className, closeAfterAction }) => ( - - ))} + {actions.map( + ({ action, isDisabled, label, className, closeAfterAction }) => ( + + ), + )} ); } diff --git a/frontend/src/components/modals/settings/SettingsForm.test.tsx b/frontend/src/components/modals/settings/SettingsForm.test.tsx index a144c06426..023571a78f 100644 --- a/frontend/src/components/modals/settings/SettingsForm.test.tsx +++ b/frontend/src/components/modals/settings/SettingsForm.test.tsx @@ -4,15 +4,22 @@ import React from "react"; import { renderWithProviders } from "test-utils"; import AgentTaskState from "#/types/AgentTaskState"; import SettingsForm from "./SettingsForm"; +import { Settings } from "#/services/settings"; const onModelChangeMock = vi.fn(); const onAgentChangeMock = vi.fn(); const onLanguageChangeMock = vi.fn(); -const renderSettingsForm = (settings: Partial) => { +const renderSettingsForm = (settings?: Settings) => { renderWithProviders( ) => { describe("SettingsForm", () => { it("should display the first values in the array by default", () => { - renderSettingsForm({}); + renderSettingsForm(); const modelInput = screen.getByRole("combobox", { name: "model" }); const agentInput = screen.getByRole("combobox", { name: "agent" }); @@ -54,7 +61,11 @@ describe("SettingsForm", () => { it("should disable settings while task is running", () => { renderWithProviders( { describe("onChange handlers", () => { it("should call the onModelChange handler when the model changes", () => { - renderSettingsForm({}); + renderSettingsForm(); const modelInput = screen.getByRole("combobox", { name: "model" }); act(() => { @@ -90,7 +101,7 @@ describe("SettingsForm", () => { }); it("should call the onAgentChange handler when the agent changes", () => { - renderSettingsForm({}); + renderSettingsForm(); const agentInput = screen.getByRole("combobox", { name: "agent" }); act(() => { @@ -106,7 +117,7 @@ describe("SettingsForm", () => { }); it("should call the onLanguageChange handler when the language changes", () => { - renderSettingsForm({}); + renderSettingsForm(); const languageInput = screen.getByRole("combobox", { name: "language" }); act(() => { diff --git a/frontend/src/components/modals/settings/SettingsForm.tsx b/frontend/src/components/modals/settings/SettingsForm.tsx index e5a82053d8..caa2d34cd1 100644 --- a/frontend/src/components/modals/settings/SettingsForm.tsx +++ b/frontend/src/components/modals/settings/SettingsForm.tsx @@ -6,9 +6,10 @@ import { I18nKey } from "../../../i18n/declaration"; import { RootState } from "../../../store"; import AgentTaskState from "../../../types/AgentTaskState"; import { AutocompleteCombobox } from "./AutocompleteCombobox"; +import { Settings } from "#/services/settings"; interface SettingsFormProps { - settings: Partial; + settings: Settings; models: string[]; agents: string[]; diff --git a/frontend/src/components/modals/settings/SettingsModal.test.tsx b/frontend/src/components/modals/settings/SettingsModal.test.tsx index 2d27eb5a35..a37502cefe 100644 --- a/frontend/src/components/modals/settings/SettingsModal.test.tsx +++ b/frontend/src/components/modals/settings/SettingsModal.test.tsx @@ -3,18 +3,32 @@ import userEvent from "@testing-library/user-event"; import React from "react"; import { renderWithProviders } from "test-utils"; import { Mock } from "vitest"; -import { - fetchAgents, - fetchModels, - getCurrentSettings, - saveSettings, -} from "#/services/settingsService"; +import i18next from "i18next"; import SettingsModal from "./SettingsModal"; +import { Settings, getSettings, saveSettings } from "#/services/settings"; +import { initializeAgent } from "#/services/agent"; +import toast from "#/utils/toast"; +import { fetchAgents, fetchModels } from "#/api"; -vi.mock("#/services/settingsService", async (importOriginal) => ({ - ...(await importOriginal()), - getCurrentSettings: vi.fn().mockReturnValue({}), +const toastSpy = vi.spyOn(toast, "settingsChanged"); +const i18nSpy = vi.spyOn(i18next, "changeLanguage"); + +vi.mock("#/services/settings", async (importOriginal) => ({ + ...(await importOriginal()), + getSettings: vi.fn().mockReturnValue({ + LLM_MODEL: "gpt-3.5-turbo", + AGENT: "MonologueAgent", + LANGUAGE: "en", + }), saveSettings: vi.fn(), +})); + +vi.mock("#/services/agent", async () => ({ + initializeAgent: vi.fn(), +})); + +vi.mock("#/api", async (importOriginal) => ({ + ...(await importOriginal()), fetchModels: vi .fn() .mockResolvedValue(Promise.resolve(["model1", "model2", "model3"])), @@ -37,10 +51,6 @@ describe("SettingsModal", () => { }); }); - it.todo( - "should display a loading spinner when fetching the models and agents", - ); - it("should close the modal when the cancel button is clicked", async () => { const onOpenChange = vi.fn(); await act(async () => @@ -58,82 +68,188 @@ describe("SettingsModal", () => { expect(onOpenChange).toHaveBeenCalledWith(false); }); - it("should call saveSettings (and close) with the new values", async () => { - const onOpenChangeMock = vi.fn(); + it("should disable the save button if the settings are the same as the initial settings", async () => { await act(async () => - renderWithProviders( - , - ), + renderWithProviders(), ); const saveButton = screen.getByRole("button", { name: /save/i }); - const modelInput = screen.getByRole("combobox", { name: "model" }); - - act(() => { - userEvent.click(modelInput); - }); - - const model3 = screen.getByText("model3"); - - act(() => { - userEvent.click(model3); - }); - - act(() => { - userEvent.click(saveButton); - }); - - expect(saveSettings).toHaveBeenCalledWith({ - LLM_MODEL: "model3", - }); - expect(onOpenChangeMock).toHaveBeenCalledWith(false); + expect(saveButton).toBeDisabled(); }); - // This test does not seem to rerender the component correctly - // Therefore, we cannot test the reset of the state - it.skip("should reset state when the cancel button is clicked", async () => { - (getCurrentSettings as Mock).mockReturnValue({ - LLM_MODEL: "model1", - AGENT: "agent1", - LANGUAGE: "English", + it("should disabled the save button if the settings contain a missing value", () => { + const onOpenChangeMock = vi.fn(); + (getSettings as Mock).mockReturnValueOnce({ + LLM_MODEL: "gpt-3.5-turbo", + AGENT: "", }); - - const onOpenChange = vi.fn(); - const { rerender } = renderWithProviders( - , + renderWithProviders( + , ); - await waitFor(() => { - expect(screen.getByRole("combobox", { name: "model" })).toHaveValue( - "model1", + const saveButton = screen.getByRole("button", { name: /save/i }); + + expect(saveButton).toBeDisabled(); + }); + + describe("onHandleSave", () => { + const initialSettings: Settings = { + LLM_MODEL: "gpt-3.5-turbo", + AGENT: "MonologueAgent", + LANGUAGE: "en", + }; + + it("should save the settings", async () => { + const onOpenChangeMock = vi.fn(); + await act(async () => + renderWithProviders( + , + ), ); + + const saveButton = screen.getByRole("button", { name: /save/i }); + const modelInput = screen.getByRole("combobox", { name: "model" }); + + act(() => { + userEvent.click(modelInput); + }); + + const model3 = screen.getByText("model3"); + + act(() => { + userEvent.click(model3); + }); + + act(() => { + userEvent.click(saveButton); + }); + + expect(saveSettings).toHaveBeenCalledWith({ + ...initialSettings, + LLM_MODEL: "model3", + }); }); - const cancelButton = screen.getByRole("button", { name: /cancel/i }); - - const modelInput = screen.getByRole("combobox", { name: "model" }); - act(() => { - userEvent.click(modelInput); - }); - - const model3 = screen.getByText("model3"); - act(() => { - userEvent.click(model3); - }); - - expect(modelInput).toHaveValue("model3"); - - act(() => { - userEvent.click(cancelButton); - }); - - rerender(); - - await waitFor(() => { - expect(screen.getByRole("combobox", { name: "model" })).toHaveValue( - "model1", + it("should reinitialize agent", async () => { + const onOpenChangeMock = vi.fn(); + await act(async () => + renderWithProviders( + , + ), ); + + const saveButton = screen.getByRole("button", { name: /save/i }); + const modelInput = screen.getByRole("combobox", { name: "model" }); + + act(() => { + userEvent.click(modelInput); + }); + + const model3 = screen.getByText("model3"); + + act(() => { + userEvent.click(model3); + }); + + act(() => { + userEvent.click(saveButton); + }); + + expect(initializeAgent).toHaveBeenCalledWith({ + ...initialSettings, + LLM_MODEL: "model3", + }); + }); + + it("should display a toast for every change", async () => { + const onOpenChangeMock = vi.fn(); + await act(async () => + renderWithProviders( + , + ), + ); + + const saveButton = screen.getByRole("button", { name: /save/i }); + const modelInput = screen.getByRole("combobox", { name: "model" }); + + act(() => { + userEvent.click(modelInput); + }); + + const model3 = screen.getByText("model3"); + + act(() => { + userEvent.click(model3); + }); + + act(() => { + userEvent.click(saveButton); + }); + + expect(toastSpy).toHaveBeenCalledTimes(1); + }); + + it("should change the language", async () => { + const onOpenChangeMock = vi.fn(); + await act(async () => + renderWithProviders( + , + ), + ); + + const saveButton = screen.getByRole("button", { name: /save/i }); + const languageInput = screen.getByRole("combobox", { name: "language" }); + + act(() => { + userEvent.click(languageInput); + }); + + const spanish = screen.getByText("EspaƱol"); + + act(() => { + userEvent.click(spanish); + }); + + act(() => { + userEvent.click(saveButton); + }); + + expect(i18nSpy).toHaveBeenCalledWith("es"); + }); + + it("should close the modal", async () => { + const onOpenChangeMock = vi.fn(); + await act(async () => + renderWithProviders( + , + ), + ); + + const saveButton = screen.getByRole("button", { name: /save/i }); + const modelInput = screen.getByRole("combobox", { name: "model" }); + + act(() => { + userEvent.click(modelInput); + }); + + const model3 = screen.getByText("model3"); + + act(() => { + userEvent.click(model3); + }); + + act(() => { + userEvent.click(saveButton); + }); + + expect(onOpenChangeMock).toHaveBeenCalledWith(false); }); }); + + it.todo("should reset setting changes when the cancel button is clicked"); + + it.todo( + "should display a loading spinner when fetching the models and agents", + ); }); diff --git a/frontend/src/components/modals/settings/SettingsModal.tsx b/frontend/src/components/modals/settings/SettingsModal.tsx index 766a0f223a..62456ef217 100644 --- a/frontend/src/components/modals/settings/SettingsModal.tsx +++ b/frontend/src/components/modals/settings/SettingsModal.tsx @@ -1,16 +1,20 @@ -import { AvailableLanguages } from "#/i18n"; -import { I18nKey } from "#/i18n/declaration"; -import { - fetchAgents, - fetchModels, - getCurrentSettings, - saveSettings, -} from "#/services/settingsService"; import { Spinner } from "@nextui-org/react"; import React from "react"; import { useTranslation } from "react-i18next"; +import i18next from "i18next"; +import { AvailableLanguages } from "#/i18n"; +import { I18nKey } from "#/i18n/declaration"; +import { fetchAgents, fetchModels } from "#/api"; import BaseModal from "../base-modal/BaseModal"; import SettingsForm from "./SettingsForm"; +import { + Settings, + saveSettings, + getSettings, + getSettingsDifference, +} from "#/services/settings"; +import toast from "#/utils/toast"; +import { initializeAgent } from "#/services/agent"; interface SettingsProps { isOpen: boolean; @@ -19,12 +23,11 @@ interface SettingsProps { function SettingsModal({ isOpen, onOpenChange }: SettingsProps) { const { t } = useTranslation(); - const currentSettings = React.useMemo(() => getCurrentSettings(), []); + const currentSettings = getSettings(); const [models, setModels] = React.useState([]); const [agents, setAgents] = React.useState([]); - const [settings, setSettings] = - React.useState>(currentSettings); + const [settings, setSettings] = React.useState(currentSettings); const [loading, setLoading] = React.useState(true); @@ -57,6 +60,17 @@ function SettingsModal({ isOpen, onOpenChange }: SettingsProps) { if (key) setSettings((prev) => ({ ...prev, LANGUAGE: key })); }; + const handleSaveSettings = () => { + const updatedSettings = getSettingsDifference(settings); + saveSettings(settings); + i18next.changeLanguage(settings.LANGUAGE); + initializeAgent(settings); // reinitialize the agent with the new settings + + Object.entries(updatedSettings).forEach(([key, value]) => { + toast.settingsChanged(`${key} set to "${value}"`); + }); + }; + return ( { - saveSettings(settings); - }, + action: handleSaveSettings, + isDisabled: + Object.values(settings).some((value) => !value) || + JSON.stringify(settings) === JSON.stringify(currentSettings), closeAfterAction: true, className: "bg-primary rounded-lg", }, diff --git a/frontend/src/services/agent.test.ts b/frontend/src/services/agent.test.ts new file mode 100644 index 0000000000..39afa86fef --- /dev/null +++ b/frontend/src/services/agent.test.ts @@ -0,0 +1,31 @@ +import { describe, it, expect, vi } from "vitest"; + +import ActionType from "#/types/ActionType"; +import { Settings } from "./settings"; +import { initializeAgent } from "./agent"; +import Socket from "./socket"; +import store from "#/store"; +import { setInitialized } from "#/state/taskSlice"; + +const sendSpy = vi.spyOn(Socket, "send"); +const dispatchSpy = vi.spyOn(store, "dispatch"); + +describe("initializeAgent", () => { + it("Should initialize the agent with the current settings", () => { + const settings: Settings = { + LLM_MODEL: "llm_value", + AGENT: "agent_value", + LANGUAGE: "language_value", + }; + + const event = { + action: ActionType.INIT, + args: settings, + }; + + initializeAgent(settings); + + expect(sendSpy).toHaveBeenCalledWith(JSON.stringify(event)); + expect(dispatchSpy).toHaveBeenCalledWith(setInitialized(false)); + }); +}); diff --git a/frontend/src/services/agent.ts b/frontend/src/services/agent.ts new file mode 100644 index 0000000000..b4c73d7703 --- /dev/null +++ b/frontend/src/services/agent.ts @@ -0,0 +1,17 @@ +import { setInitialized } from "#/state/taskSlice"; +import store from "#/store"; +import ActionType from "#/types/ActionType"; +import { Settings } from "./settings"; +import Socket from "./socket"; + +/** + * Initialize the agent with the current settings. + * @param settings - The new settings. + */ +export const initializeAgent = (settings: Settings) => { + const event = { action: ActionType.INIT, args: settings }; + const eventString = JSON.stringify(event); + + store.dispatch(setInitialized(false)); + Socket.send(eventString); +}; diff --git a/frontend/src/services/settings.d.ts b/frontend/src/services/settings.d.ts deleted file mode 100644 index de87de8b2a..0000000000 --- a/frontend/src/services/settings.d.ts +++ /dev/null @@ -1,5 +0,0 @@ -type Settings = { - LLM_MODEL: string; - AGENT: string; - LANGUAGE: string; -}; diff --git a/frontend/src/services/settings.test.ts b/frontend/src/services/settings.test.ts new file mode 100644 index 0000000000..caef2486c2 --- /dev/null +++ b/frontend/src/services/settings.test.ts @@ -0,0 +1,137 @@ +import { describe, expect, it, vi, Mock } from "vitest"; +import { + DEFAULT_SETTINGS, + getSettings, + getSettingsDifference, + saveSettings, +} from "./settings"; + +Storage.prototype.getItem = vi.fn(); +Storage.prototype.setItem = vi.fn(); + +afterEach(() => { + vi.resetAllMocks(); +}); + +describe("getSettings", () => { + it("should get the stored settings", () => { + (localStorage.getItem as Mock) + .mockReturnValueOnce("llm_value") + .mockReturnValueOnce("agent_value") + .mockReturnValueOnce("language_value"); + + const settings = getSettings(); + + expect(settings).toEqual({ + LLM_MODEL: "llm_value", + AGENT: "agent_value", + LANGUAGE: "language_value", + }); + }); + + it("should handle return defaults if localStorage key does not exist", () => { + (localStorage.getItem as Mock) + .mockReturnValueOnce(null) + .mockReturnValueOnce(null) + .mockReturnValueOnce(null); + + const settings = getSettings(); + + expect(settings).toEqual({ + LLM_MODEL: DEFAULT_SETTINGS.LLM_MODEL, + AGENT: DEFAULT_SETTINGS.AGENT, + LANGUAGE: DEFAULT_SETTINGS.LANGUAGE, + }); + }); +}); + +describe("saveSettings", () => { + it("should save the settings", () => { + const settings = { + LLM_MODEL: "llm_value", + AGENT: "agent_value", + LANGUAGE: "language_value", + }; + + saveSettings(settings); + + expect(localStorage.setItem).toHaveBeenCalledWith("LLM_MODEL", "llm_value"); + expect(localStorage.setItem).toHaveBeenCalledWith("AGENT", "agent_value"); + expect(localStorage.setItem).toHaveBeenCalledWith( + "LANGUAGE", + "language_value", + ); + }); + + it("should save partial settings", () => { + const settings = { + LLM_MODEL: "llm_value", + }; + + saveSettings(settings); + + expect(localStorage.setItem).toHaveBeenCalledOnce(); + expect(localStorage.setItem).toHaveBeenCalledWith("LLM_MODEL", "llm_value"); + }); + + it("should not save invalid settings", () => { + const settings = { + LLM_MODEL: "llm_value", + AGENT: "agent_value", + LANGUAGE: "language_value", + INVALID: "invalid_value", + }; + + saveSettings(settings); + + expect(localStorage.setItem).toHaveBeenCalledWith("LLM_MODEL", "llm_value"); + expect(localStorage.setItem).toHaveBeenCalledWith("AGENT", "agent_value"); + expect(localStorage.setItem).toHaveBeenCalledWith( + "LANGUAGE", + "language_value", + ); + expect(localStorage.setItem).not.toHaveBeenCalledWith( + "INVALID", + "invalid_value", + ); + }); +}); + +describe("getSettingsDifference", () => { + beforeEach(() => { + (localStorage.getItem as Mock) + .mockReturnValueOnce("llm_value") + .mockReturnValueOnce("agent_value") + .mockReturnValueOnce("language_value"); + }); + + it("should return updated settings", () => { + const settings = { + LLM_MODEL: "new_llm_value", + AGENT: "new_agent_value", + LANGUAGE: "language_value", + }; + + const updatedSettings = getSettingsDifference(settings); + + expect(updatedSettings).toEqual({ + LLM_MODEL: "new_llm_value", + AGENT: "new_agent_value", + }); + }); + + it("should not handle invalid settings", () => { + const settings = { + LLM_MODEL: "new_llm_value", + AGENT: "new_agent_value", + INVALID: "invalid_value", + }; + + const updatedSettings = getSettingsDifference(settings); + + expect(updatedSettings).toEqual({ + LLM_MODEL: "new_llm_value", + AGENT: "new_agent_value", + }); + }); +}); diff --git a/frontend/src/services/settings.ts b/frontend/src/services/settings.ts new file mode 100644 index 0000000000..651e2defde --- /dev/null +++ b/frontend/src/services/settings.ts @@ -0,0 +1,63 @@ +export type Settings = { + LLM_MODEL: string; + AGENT: string; + LANGUAGE: string; +}; + +export const DEFAULT_SETTINGS: Settings = { + LLM_MODEL: "gpt-3.5-turbo", + AGENT: "MonologueAgent", + LANGUAGE: "en", +}; + +const validKeys = Object.keys(DEFAULT_SETTINGS) as (keyof Settings)[]; + +/** + * Get the settings from local storage or use the default settings if not found + */ +export const getSettings = (): Settings => ({ + LLM_MODEL: localStorage.getItem("LLM_MODEL") || DEFAULT_SETTINGS.LLM_MODEL, + AGENT: localStorage.getItem("AGENT") || DEFAULT_SETTINGS.AGENT, + LANGUAGE: localStorage.getItem("LANGUAGE") || DEFAULT_SETTINGS.LANGUAGE, +}); + +/** + * Save the settings to local storage. Only valid settings are saved. + * @param settings - the settings to save + */ +export const saveSettings = (settings: Partial) => { + Object.keys(settings).forEach((key) => { + const isValid = validKeys.includes(key as keyof Settings); + const value = settings[key as keyof Settings]; + + if (isValid && value) localStorage.setItem(key, value); + }); +}; + +/** + * Get the difference between the current settings and the provided settings. + * Useful for notifiying the user of exact changes. + * + * @example + * // Assuming the current settings are: { LLM_MODEL: "gpt-3.5", AGENT: "MonologueAgent", LANGUAGE: "en" } + * const updatedSettings = getSettingsDifference({ LLM_MODEL: "gpt-3.5", AGENT: "OTHER_AGENT", LANGUAGE: "en" }); + * // updatedSettings = { AGENT: "OTHER_AGENT" } + * + * @param settings - the settings to compare + * @returns the updated settings + */ +export const getSettingsDifference = (settings: Partial) => { + const currentSettings = getSettings(); + const updatedSettings: Partial = {}; + + Object.keys(settings).forEach((key) => { + if ( + validKeys.includes(key as keyof Settings) && + settings[key as keyof Settings] !== currentSettings[key as keyof Settings] + ) { + updatedSettings[key as keyof Settings] = settings[key as keyof Settings]; + } + }); + + return updatedSettings; +}; diff --git a/frontend/src/services/settingsService.test.ts b/frontend/src/services/settingsService.test.ts deleted file mode 100644 index ce9861273f..0000000000 --- a/frontend/src/services/settingsService.test.ts +++ /dev/null @@ -1,101 +0,0 @@ -import { ArgConfigType } from "#/types/ConfigType"; -import { getSettingOrDefault, getUpdatedSettings } from "./settingsService"; - -Storage.prototype.getItem = vi.fn(); - -describe("getSettingOrDefault", () => { - it("should return the value from localStorage if it exists", () => { - (localStorage.getItem as jest.Mock).mockReturnValue("value"); - const result = getSettingOrDefault("some_key"); - - expect(result).toEqual("value"); - }); - - it("should return the default value if localStorage does not exist", () => { - (localStorage.getItem as jest.Mock).mockReturnValue(null); - const result = getSettingOrDefault("LLM_MODEL"); - - expect(result).toEqual("gpt-3.5-turbo"); - }); -}); - -describe("getUpdatedSettings", () => { - it("should return initial settings if newSettings is empty", () => { - const oldSettings = { key1: "value1" }; - - const result = getUpdatedSettings({}, oldSettings); - - expect(result).toStrictEqual({}); - }); - - it("should update settings", () => { - const oldSettings = { - [ArgConfigType.LLM_MODEL]: "gpt-4-0125-preview", - [ArgConfigType.AGENT]: "MonologueAgent", - [ArgConfigType.LANGUAGE]: "en", - }; - const newSettings = { - [ArgConfigType.AGENT]: "OtherAgent", - }; - - const result = getUpdatedSettings(newSettings, oldSettings); - - expect(result).toStrictEqual({ - [ArgConfigType.AGENT]: "OtherAgent", - }); - }); - - it("should show no values if they are equal", () => { - const oldSettings = { - [ArgConfigType.LLM_MODEL]: "gpt-4-0125-preview", - [ArgConfigType.AGENT]: "MonologueAgent", - }; - const newSettings = { - [ArgConfigType.AGENT]: "MonologueAgent", - }; - - const result = getUpdatedSettings(newSettings, oldSettings); - - expect(result).toStrictEqual({}); - }); - - it("should update all settings", () => { - const oldSettings = { - [ArgConfigType.LLM_MODEL]: "gpt-4-0125-preview", - [ArgConfigType.AGENT]: "MonologueAgent", - key1: "value1", - }; - const newSettings = { - [ArgConfigType.AGENT]: "CodeActAgent", - [ArgConfigType.LANGUAGE]: "es", - key1: "newvalue1", - key2: "value2", - }; - - const result = getUpdatedSettings(newSettings, oldSettings); - - expect(result).toStrictEqual({ - [ArgConfigType.AGENT]: "CodeActAgent", - [ArgConfigType.LANGUAGE]: "es", - }); - }); - - it("should not update settings that are not supported", () => { - const oldSettings = { - [ArgConfigType.LLM_MODEL]: "gpt-4-0125-preview", - [ArgConfigType.AGENT]: "MonologueAgent", - }; - const newSettings = { - [ArgConfigType.LLM_MODEL]: "gpt-4-0125-preview", - [ArgConfigType.AGENT]: "CodeActAgent", - key1: "newvalue1", - key2: "value2", - }; - - const result = getUpdatedSettings(newSettings, oldSettings); - - expect(result).toStrictEqual({ - [ArgConfigType.AGENT]: "CodeActAgent", - }); - }); -}); diff --git a/frontend/src/services/settingsService.ts b/frontend/src/services/settingsService.ts deleted file mode 100644 index 0ebf3466d2..0000000000 --- a/frontend/src/services/settingsService.ts +++ /dev/null @@ -1,94 +0,0 @@ -import { setInitialized } from "#/state/taskSlice"; -import store from "#/store"; -import ActionType from "#/types/ActionType"; -import { SupportedSettings } from "#/types/ConfigType"; -import { setByKey } from "#/state/settingsSlice"; -import toast from "#/utils/toast"; -import Socket from "./socket"; - -export type Settings = { [key: string]: string }; - -export async function fetchModels() { - const response = await fetch(`/api/litellm-models`); - return response.json(); -} - -export async function fetchAgents() { - const response = await fetch(`/api/agents`); - return response.json(); -} - -// all available settings in the frontend -// TODO: add the values to i18n to support multi languages -const DISPLAY_MAP: { [key: string]: string } = { - LLM_MODEL: "model", - AGENT: "agent", - LANGUAGE: "language", -}; - -const DEFAULT_SETTINGS: Settings = { - LLM_MODEL: "gpt-3.5-turbo", - AGENT: "MonologueAgent", - LANGUAGE: "en", -}; - -export const getSettingOrDefault = (key: string): string => { - const value = localStorage.getItem(key); - return value || DEFAULT_SETTINGS[key]; -}; - -export const getCurrentSettings = (): Settings => ({ - LLM_MODEL: getSettingOrDefault("LLM_MODEL"), - AGENT: getSettingOrDefault("AGENT"), - LANGUAGE: getSettingOrDefault("LANGUAGE"), -}); - -// Function to merge and update settings -export const getUpdatedSettings = ( - newSettings: Settings, - currentSettings: Settings, -) => { - const updatedSettings: Settings = {}; - SupportedSettings.forEach((setting) => { - if ( - newSettings[setting] !== currentSettings[setting] && - !!newSettings[setting] // check if the value is not empty/undefined - ) { - updatedSettings[setting] = newSettings[setting]; - } - }); - return updatedSettings; -}; - -export const dispatchSettings = (updatedSettings: Record) => { - let i = 0; - for (const [key, value] of Object.entries(updatedSettings)) { - store.dispatch(setByKey({ key, value })); - if (key in DISPLAY_MAP) { - setTimeout(() => { - toast.settingsChanged(`Set ${DISPLAY_MAP[key]} to "${value}"`); - }, i * 500); - i += 1; - } - } -}; - -export const initializeAgent = () => { - const event = { action: ActionType.INIT, args: getCurrentSettings() }; - const eventString = JSON.stringify(event); - store.dispatch(setInitialized(false)); - Socket.send(eventString); -}; - -// Save and send settings to the server -export function saveSettings(newSettings: Settings): void { - const currentSettings = getCurrentSettings(); - const updatedSettings = getUpdatedSettings(newSettings, currentSettings); - - if (Object.keys(updatedSettings).length === 0) { - return; - } - - dispatchSettings(updatedSettings); - initializeAgent(); -} diff --git a/frontend/src/state/settingsSlice.ts b/frontend/src/state/settingsSlice.ts deleted file mode 100644 index d39ccb1689..0000000000 --- a/frontend/src/state/settingsSlice.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { createSlice } from "@reduxjs/toolkit"; -import i18next from "i18next"; -import { ArgConfigType } from "#/types/ConfigType"; - -export const settingsSlice = createSlice({ - name: "settings", - initialState: { - ALL_SETTINGS: localStorage.getItem("ALL_SETTINGS") || "", - [ArgConfigType.LLM_MODEL]: - localStorage.getItem(ArgConfigType.LLM_MODEL) || "", - [ArgConfigType.AGENT]: localStorage.getItem(ArgConfigType.AGENT) || "", - [ArgConfigType.LANGUAGE]: - localStorage.getItem(ArgConfigType.LANGUAGE) || "en", - } as { [key: string]: string }, - reducers: { - setByKey: (state, action) => { - const { key, value } = action.payload; - state[key] = value; - localStorage.setItem(key, value); - // language is a special case for now. - if (key === ArgConfigType.LANGUAGE) { - i18next.changeLanguage(value); - } - }, - setAllSettings: (state, action) => { - state.ALL_SETTINGS = action.payload; - localStorage.setItem("ALL_SETTINGS", action.payload); - }, - }, -}); - -export const { setByKey, setAllSettings } = settingsSlice.actions; - -export default settingsSlice.reducer; diff --git a/frontend/src/store.ts b/frontend/src/store.ts index 5e717d9940..05ea2fe509 100644 --- a/frontend/src/store.ts +++ b/frontend/src/store.ts @@ -6,7 +6,6 @@ import codeReducer from "./state/codeSlice"; import commandReducer from "./state/commandSlice"; import errorsReducer from "./state/errorsSlice"; import planReducer from "./state/planSlice"; -import settingsReducer from "./state/settingsSlice"; import taskReducer from "./state/taskSlice"; export const rootReducer = combineReducers({ @@ -16,7 +15,6 @@ export const rootReducer = combineReducers({ cmd: commandReducer, task: taskReducer, errors: errorsReducer, - settings: settingsReducer, plan: planReducer, agent: agentReducer, });