refactor(frontend): settings and friends (#1400)

* refactor settings and friends

* extend base modal to support disabled property

* extend settings handler to change language via i18next

* remove unused settings.d.ts

---------

Co-authored-by: Jim Su <jimsu@protonmail.com>
Co-authored-by: Engel Nyst <enyst@users.noreply.github.com>
This commit is contained in:
sp.wack 2024-04-29 04:59:26 +03:00 committed by GitHub
parent 24b71927c3
commit 8954855ba3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 553 additions and 348 deletions

View File

@ -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]);

View File

@ -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();
}

View File

@ -96,6 +96,42 @@ describe("BaseModal", () => {
expect(screen.getByText("Children")).toBeInTheDocument();
});
it("should disable the action given the condition", () => {
const { rerender } = render(
<BaseModal
isOpen
onOpenChange={vi.fn}
title="Settings"
actions={[
{
label: "Save",
action: () => {},
isDisabled: true,
},
]}
/>,
);
expect(screen.getByText("Save")).toBeDisabled();
rerender(
<BaseModal
isOpen
onOpenChange={vi.fn}
title="Settings"
actions={[
{
label: "Save",
action: () => {},
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(

View File

@ -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 }) => (
<Button
key={label}
type="button"
onClick={() => {
action();
if (closeAfterAction) closeModal();
}}
className={className}
>
{label}
</Button>
))}
{actions.map(
({ action, isDisabled, label, className, closeAfterAction }) => (
<Button
key={label}
type="button"
isDisabled={isDisabled}
onClick={() => {
action();
if (closeAfterAction) closeModal();
}}
className={className}
>
{label}
</Button>
),
)}
</>
);
}

View File

@ -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<Settings>) => {
const renderSettingsForm = (settings?: Settings) => {
renderWithProviders(
<SettingsForm
settings={settings}
settings={
settings || {
LLM_MODEL: "model1",
AGENT: "agent1",
LANGUAGE: "en",
}
}
models={["model1", "model2", "model3"]}
agents={["agent1", "agent2", "agent3"]}
onModelChange={onModelChangeMock}
@ -24,7 +31,7 @@ const renderSettingsForm = (settings: Partial<Settings>) => {
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(
<SettingsForm
settings={{}}
settings={{
LLM_MODEL: "model1",
AGENT: "agent1",
LANGUAGE: "en",
}}
models={["model1", "model2", "model3"]}
agents={["agent1", "agent2", "agent3"]}
onModelChange={onModelChangeMock}
@ -74,7 +85,7 @@ describe("SettingsForm", () => {
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(() => {

View File

@ -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: Settings;
models: string[];
agents: string[];

View File

@ -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<typeof import("#/services/settingsService")>()),
getCurrentSettings: vi.fn().mockReturnValue({}),
const toastSpy = vi.spyOn(toast, "settingsChanged");
const i18nSpy = vi.spyOn(i18next, "changeLanguage");
vi.mock("#/services/settings", async (importOriginal) => ({
...(await importOriginal<typeof import("#/services/settings")>()),
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<typeof import("#/api")>()),
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(
<SettingsModal isOpen onOpenChange={onOpenChangeMock} />,
),
renderWithProviders(<SettingsModal isOpen onOpenChange={vi.fn()} />),
);
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(
<SettingsModal isOpen onOpenChange={onOpenChange} />,
renderWithProviders(
<SettingsModal isOpen onOpenChange={onOpenChangeMock} />,
);
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(
<SettingsModal isOpen onOpenChange={onOpenChangeMock} />,
),
);
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(<SettingsModal isOpen onOpenChange={onOpenChange} />);
await waitFor(() => {
expect(screen.getByRole("combobox", { name: "model" })).toHaveValue(
"model1",
it("should reinitialize agent", async () => {
const onOpenChangeMock = vi.fn();
await act(async () =>
renderWithProviders(
<SettingsModal isOpen onOpenChange={onOpenChangeMock} />,
),
);
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(
<SettingsModal isOpen onOpenChange={onOpenChangeMock} />,
),
);
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(
<SettingsModal isOpen onOpenChange={onOpenChangeMock} />,
),
);
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(
<SettingsModal isOpen onOpenChange={onOpenChangeMock} />,
),
);
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",
);
});

View File

@ -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<string[]>([]);
const [agents, setAgents] = React.useState<string[]>([]);
const [settings, setSettings] =
React.useState<Partial<Settings>>(currentSettings);
const [settings, setSettings] = React.useState<Settings>(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 (
<BaseModal
isOpen={isOpen}
@ -66,9 +80,10 @@ function SettingsModal({ isOpen, onOpenChange }: SettingsProps) {
actions={[
{
label: t(I18nKey.CONFIGURATION$MODAL_SAVE_BUTTON_LABEL),
action: () => {
saveSettings(settings);
},
action: handleSaveSettings,
isDisabled:
Object.values(settings).some((value) => !value) ||
JSON.stringify(settings) === JSON.stringify(currentSettings),
closeAfterAction: true,
className: "bg-primary rounded-lg",
},

View File

@ -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));
});
});

View File

@ -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);
};

View File

@ -1,5 +0,0 @@
type Settings = {
LLM_MODEL: string;
AGENT: string;
LANGUAGE: string;
};

View File

@ -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",
});
});
});

View File

@ -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<Settings>) => {
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<Settings>) => {
const currentSettings = getSettings();
const updatedSettings: Partial<Settings> = {};
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;
};

View File

@ -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",
});
});
});

View File

@ -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<string, string>) => {
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();
}

View File

@ -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;

View File

@ -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,
});