mirror of
https://github.com/OpenHands/OpenHands.git
synced 2025-12-26 05:48:36 +08:00
chore(frontend): Remove old session class and some artifacts that are no longer needed (#4310)
This commit is contained in:
parent
63ff69fd97
commit
70bd710e82
@ -1,10 +1,8 @@
|
||||
import { screen, act } from "@testing-library/react";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { renderWithProviders } from "test-utils";
|
||||
import { createMemoryRouter, RouterProvider } from "react-router-dom";
|
||||
import Session from "#/services/session";
|
||||
import ActionType from "#/types/ActionType";
|
||||
import { addAssistantMessage } from "#/state/chatSlice";
|
||||
import AgentState from "#/types/AgentState";
|
||||
import ChatInterface from "#/components/chat/ChatInterface";
|
||||
@ -34,9 +32,6 @@ HTMLElement.prototype.scrollTo = vi.fn().mockImplementation(() => {});
|
||||
const TEST_TIMESTAMP = new Date().toISOString();
|
||||
|
||||
describe.skip("ChatInterface", () => {
|
||||
const sessionSendSpy = vi.spyOn(Session, "send");
|
||||
vi.spyOn(Session, "isConnected").mockReturnValue(true);
|
||||
|
||||
// TODO: replace below with e.g. fake timers
|
||||
// https://vitest.dev/guide/mocking#timers
|
||||
// https://vitest.dev/api/vi.html#vi-usefaketimers
|
||||
@ -66,19 +61,6 @@ describe.skip("ChatInterface", () => {
|
||||
},
|
||||
});
|
||||
|
||||
const userMessageEvent = {
|
||||
action: ActionType.MESSAGE,
|
||||
args: {
|
||||
content: "my message",
|
||||
images_urls: [],
|
||||
timestamp: TEST_TIMESTAMP,
|
||||
},
|
||||
};
|
||||
|
||||
afterEach(() => {
|
||||
sessionSendSpy.mockClear();
|
||||
});
|
||||
|
||||
it("should render empty message list and input", () => {
|
||||
renderWithProviders(<ChatInterface />);
|
||||
expect(screen.queryAllByTestId("article")).toHaveLength(0);
|
||||
@ -125,10 +107,6 @@ describe.skip("ChatInterface", () => {
|
||||
const input = screen.getByRole("textbox");
|
||||
await user.type(input, "my message");
|
||||
await user.keyboard("{Enter}");
|
||||
|
||||
expect(sessionSendSpy).toHaveBeenCalledWith(
|
||||
expect.toMatchMessageEvent(JSON.stringify(userMessageEvent)),
|
||||
);
|
||||
});
|
||||
|
||||
it("should send the user message as an event to the Session when the agent state is AWAITING_USER_INPUT", async () => {
|
||||
@ -144,10 +122,6 @@ describe.skip("ChatInterface", () => {
|
||||
const input = screen.getByRole("textbox");
|
||||
await user.type(input, "my message");
|
||||
await user.keyboard("{Enter}");
|
||||
|
||||
expect(sessionSendSpy).toHaveBeenCalledWith(
|
||||
expect.toMatchMessageEvent(JSON.stringify(userMessageEvent)),
|
||||
);
|
||||
});
|
||||
|
||||
it("should disable the user input if agent is not initialized", async () => {
|
||||
@ -168,7 +142,6 @@ describe.skip("ChatInterface", () => {
|
||||
);
|
||||
|
||||
expect(submitButton).toBeDisabled();
|
||||
expect(sessionSendSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it.todo("test scroll-related behaviour");
|
||||
|
||||
@ -1,26 +0,0 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { userEvent } from "@testing-library/user-event";
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import AgentState from "#/types/AgentState";
|
||||
import { changeAgentState } from "#/services/agentStateService";
|
||||
import ConfirmationButtons from "#/components/chat/ConfirmationButtons";
|
||||
|
||||
describe("ConfirmationButtons", () => {
|
||||
vi.mock("#/services/agentStateService", () => ({
|
||||
changeAgentState: vi.fn(),
|
||||
}));
|
||||
|
||||
it.skip("should change agent state appropriately on button click", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<ConfirmationButtons />);
|
||||
|
||||
const confirmButton = screen.getByTestId("action-confirm-button");
|
||||
const rejectButton = screen.getByTestId("action-reject-button");
|
||||
|
||||
await user.click(confirmButton);
|
||||
expect(changeAgentState).toHaveBeenCalledWith(AgentState.USER_CONFIRMED);
|
||||
|
||||
await user.click(rejectButton);
|
||||
expect(changeAgentState).toHaveBeenCalledWith(AgentState.USER_REJECTED);
|
||||
});
|
||||
});
|
||||
@ -1,82 +0,0 @@
|
||||
import { act, render, screen } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { AutocompleteCombobox } from "#/components/modals/settings/AutocompleteCombobox";
|
||||
|
||||
const onChangeMock = vi.fn();
|
||||
|
||||
const renderComponent = () =>
|
||||
render(
|
||||
<AutocompleteCombobox
|
||||
ariaLabel="model"
|
||||
items={[
|
||||
{ value: "m1", label: "model1" },
|
||||
{ value: "m2", label: "model2" },
|
||||
{ value: "m3", label: "model3" },
|
||||
]}
|
||||
defaultKey="m1"
|
||||
tooltip="tooltip"
|
||||
onChange={onChangeMock}
|
||||
/>,
|
||||
);
|
||||
|
||||
describe("AutocompleteCombobox", () => {
|
||||
it("should render a combobox with the default value", () => {
|
||||
renderComponent();
|
||||
|
||||
const modelInput = screen.getByRole("combobox", { name: "model" });
|
||||
expect(modelInput).toHaveValue("model1");
|
||||
});
|
||||
|
||||
it("should open a dropdown with the available values", async () => {
|
||||
renderComponent();
|
||||
|
||||
const modelInput = screen.getByRole("combobox", { name: "model" });
|
||||
|
||||
expect(screen.queryByText("model2")).not.toBeInTheDocument();
|
||||
expect(screen.queryByText("model3")).not.toBeInTheDocument();
|
||||
|
||||
await act(async () => {
|
||||
await userEvent.click(modelInput);
|
||||
});
|
||||
|
||||
expect(screen.getByText("model2")).toBeInTheDocument();
|
||||
expect(screen.getByText("model3")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should call the onChange handler when a new value is selected", async () => {
|
||||
renderComponent();
|
||||
|
||||
const modelInput = screen.getByRole("combobox", { name: "model" });
|
||||
expect(modelInput).toHaveValue("model1");
|
||||
|
||||
await act(async () => {
|
||||
await userEvent.click(modelInput);
|
||||
});
|
||||
|
||||
const model2 = screen.getByText("model2");
|
||||
await act(async () => {
|
||||
await userEvent.click(model2);
|
||||
});
|
||||
|
||||
expect(onChangeMock).toHaveBeenCalledWith("model2");
|
||||
});
|
||||
|
||||
it("should set the input value to the default key if the default key is not in the list", () => {
|
||||
render(
|
||||
<AutocompleteCombobox
|
||||
ariaLabel="model"
|
||||
items={[{ value: "m1", label: "model1" }]}
|
||||
defaultKey="m2"
|
||||
tooltip="tooltip"
|
||||
onChange={onChangeMock}
|
||||
/>,
|
||||
);
|
||||
|
||||
const modelInput = screen.getByRole("combobox", { name: "model" });
|
||||
|
||||
expect(modelInput).toHaveValue("m2");
|
||||
});
|
||||
|
||||
it.todo("should show a tooltip after 0.5 seconds of focus");
|
||||
});
|
||||
@ -1,41 +0,0 @@
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import ActionType from "#/types/ActionType";
|
||||
import { Settings, saveSettings } from "../../src/services/settings";
|
||||
import Session from "../../src/services/session";
|
||||
|
||||
const sendSpy = vi.spyOn(Session, "send");
|
||||
// @ts-expect-error - spying on private function
|
||||
const setupSpy = vi.spyOn(Session, "_setupSocket").mockImplementation(() => {
|
||||
// @ts-expect-error - calling a private function
|
||||
Session._initializeAgent();
|
||||
});
|
||||
|
||||
describe("startNewSession", () => {
|
||||
afterEach(() => {
|
||||
sendSpy.mockClear();
|
||||
setupSpy.mockClear();
|
||||
});
|
||||
|
||||
it("Should start a new session with the current settings", () => {
|
||||
const settings: Settings = {
|
||||
LLM_MODEL: "llm_value",
|
||||
LLM_BASE_URL: "base_url",
|
||||
AGENT: "agent_value",
|
||||
LANGUAGE: "language_value",
|
||||
LLM_API_KEY: "sk-...",
|
||||
CONFIRMATION_MODE: true,
|
||||
SECURITY_ANALYZER: "analyzer",
|
||||
};
|
||||
|
||||
const event = {
|
||||
action: ActionType.INIT,
|
||||
args: settings,
|
||||
};
|
||||
|
||||
saveSettings(settings);
|
||||
Session.startNewSession();
|
||||
|
||||
expect(setupSpy).toHaveBeenCalledTimes(1);
|
||||
expect(sendSpy).toHaveBeenCalledWith(JSON.stringify(event));
|
||||
});
|
||||
});
|
||||
@ -1,81 +0,0 @@
|
||||
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" | "securityanalyzer";
|
||||
|
||||
const LABELS: Record<Label, I18nKey> = {
|
||||
model: I18nKey.CONFIGURATION$MODEL_SELECT_LABEL,
|
||||
agent: I18nKey.CONFIGURATION$AGENT_SELECT_LABEL,
|
||||
language: I18nKey.CONFIGURATION$LANGUAGE_SELECT_LABEL,
|
||||
securityanalyzer: I18nKey.CONFIGURATION$SECURITY_SELECT_LABEL,
|
||||
};
|
||||
|
||||
const PLACEHOLDERS: Record<Label, I18nKey> = {
|
||||
model: I18nKey.CONFIGURATION$MODEL_SELECT_PLACEHOLDER,
|
||||
agent: I18nKey.CONFIGURATION$AGENT_SELECT_PLACEHOLDER,
|
||||
language: I18nKey.CONFIGURATION$LANGUAGE_SELECT_PLACEHOLDER,
|
||||
securityanalyzer: I18nKey.CONFIGURATION$SECURITY_SELECT_PLACEHOLDER,
|
||||
};
|
||||
|
||||
type AutocompleteItemType = {
|
||||
value: string;
|
||||
label: string;
|
||||
};
|
||||
|
||||
interface AutocompleteComboboxProps {
|
||||
ariaLabel: Label;
|
||||
items: AutocompleteItemType[];
|
||||
defaultKey: string;
|
||||
onChange: (key: string) => void;
|
||||
tooltip: string;
|
||||
allowCustomValue?: boolean;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export function AutocompleteCombobox({
|
||||
ariaLabel,
|
||||
items,
|
||||
defaultKey,
|
||||
onChange,
|
||||
tooltip,
|
||||
allowCustomValue = false,
|
||||
disabled = false,
|
||||
}: AutocompleteComboboxProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
content={
|
||||
disabled
|
||||
? `${tooltip} ${t(I18nKey.SETTINGS$DISABLED_RUNNING)}`
|
||||
: tooltip
|
||||
}
|
||||
closeDelay={100}
|
||||
delay={500}
|
||||
>
|
||||
<Autocomplete
|
||||
aria-label={ariaLabel}
|
||||
label={t(LABELS[ariaLabel])}
|
||||
placeholder={t(PLACEHOLDERS[ariaLabel])}
|
||||
defaultItems={items}
|
||||
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}
|
||||
>
|
||||
{(item) => (
|
||||
<AutocompleteItem key={item.value}>{item.label}</AutocompleteItem>
|
||||
)}
|
||||
</Autocomplete>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
@ -1,184 +0,0 @@
|
||||
import { Input, Switch, Tooltip, useDisclosure } from "@nextui-org/react";
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { FaEye, FaEyeSlash } from "react-icons/fa";
|
||||
import { AvailableLanguages } from "../../../i18n";
|
||||
import { I18nKey } from "../../../i18n/declaration";
|
||||
import { AutocompleteCombobox } from "./AutocompleteCombobox";
|
||||
import { Settings } from "#/services/settings";
|
||||
import { organizeModelsAndProviders } from "#/utils/organizeModelsAndProviders";
|
||||
import { extractModelAndProvider } from "#/utils/extractModelAndProvider";
|
||||
import { ModelSelector } from "./ModelSelector";
|
||||
|
||||
interface SettingsFormProps {
|
||||
settings: Settings;
|
||||
models: string[];
|
||||
agents: string[];
|
||||
securityAnalyzers: string[];
|
||||
disabled: boolean;
|
||||
|
||||
onModelChange: (model: string) => void;
|
||||
onBaseURLChange: (baseURL: string) => void;
|
||||
onAPIKeyChange: (apiKey: string) => void;
|
||||
onAgentChange: (agent: string) => void;
|
||||
onLanguageChange: (language: string) => void;
|
||||
onConfirmationModeChange: (confirmationMode: boolean) => void;
|
||||
onSecurityAnalyzerChange: (securityAnalyzer: string) => void;
|
||||
}
|
||||
|
||||
function SettingsForm({
|
||||
settings,
|
||||
models,
|
||||
agents,
|
||||
securityAnalyzers,
|
||||
disabled,
|
||||
onModelChange,
|
||||
onBaseURLChange,
|
||||
onAPIKeyChange,
|
||||
onAgentChange,
|
||||
onLanguageChange,
|
||||
onConfirmationModeChange,
|
||||
onSecurityAnalyzerChange,
|
||||
}: SettingsFormProps) {
|
||||
const { t } = useTranslation();
|
||||
const { isOpen: isVisible, onOpenChange: onVisibleChange } = useDisclosure();
|
||||
const advancedAlreadyInUse = React.useMemo(() => {
|
||||
const organizedModels = organizeModelsAndProviders(models);
|
||||
const { provider, model } = extractModelAndProvider(
|
||||
settings.LLM_MODEL || "",
|
||||
);
|
||||
const isKnownModel =
|
||||
provider in organizedModels &&
|
||||
organizedModels[provider].models.includes(model);
|
||||
|
||||
return (
|
||||
!!settings.SECURITY_ANALYZER ||
|
||||
!!settings.CONFIRMATION_MODE ||
|
||||
!!settings.LLM_BASE_URL ||
|
||||
(!!settings.LLM_MODEL && !isKnownModel)
|
||||
);
|
||||
}, [settings, models]);
|
||||
const [enableAdvanced, setEnableAdvanced] =
|
||||
React.useState(advancedAlreadyInUse);
|
||||
|
||||
React.useEffect(() => {
|
||||
setEnableAdvanced(advancedAlreadyInUse);
|
||||
}, [advancedAlreadyInUse]);
|
||||
|
||||
const handleAdvancedChange = (value: boolean) => {
|
||||
setEnableAdvanced(value);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Switch
|
||||
data-testid="advanced-options-toggle"
|
||||
aria-checked={enableAdvanced}
|
||||
isSelected={enableAdvanced}
|
||||
onValueChange={handleAdvancedChange}
|
||||
>
|
||||
Advanced Options
|
||||
</Switch>
|
||||
{enableAdvanced && (
|
||||
<>
|
||||
<Input
|
||||
data-testid="custom-model-input"
|
||||
label="Custom Model"
|
||||
onValueChange={onModelChange}
|
||||
defaultValue={settings.LLM_MODEL}
|
||||
/>
|
||||
<Input
|
||||
data-testid="base-url-input"
|
||||
label="Base URL"
|
||||
onValueChange={onBaseURLChange}
|
||||
defaultValue={settings.LLM_BASE_URL}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{!enableAdvanced && (
|
||||
<ModelSelector
|
||||
isDisabled={disabled}
|
||||
models={organizeModelsAndProviders(models)}
|
||||
currentModel={settings.LLM_MODEL}
|
||||
/>
|
||||
)}
|
||||
<Input
|
||||
label="API Key"
|
||||
isDisabled={disabled}
|
||||
aria-label="apikey"
|
||||
data-testid="apikey"
|
||||
placeholder={t(I18nKey.SETTINGS$API_KEY_PLACEHOLDER)}
|
||||
type={isVisible ? "text" : "password"}
|
||||
value={settings.LLM_API_KEY || ""}
|
||||
onChange={(e) => {
|
||||
onAPIKeyChange(e.target.value);
|
||||
}}
|
||||
endContent={
|
||||
<button
|
||||
className="focus:outline-none"
|
||||
type="button"
|
||||
onClick={onVisibleChange}
|
||||
>
|
||||
{isVisible ? (
|
||||
<FaEye className="text-2xl text-default-400 pointer-events-none" />
|
||||
) : (
|
||||
<FaEyeSlash className="text-2xl text-default-400 pointer-events-none" />
|
||||
)}
|
||||
</button>
|
||||
}
|
||||
/>
|
||||
<AutocompleteCombobox
|
||||
ariaLabel="language"
|
||||
items={AvailableLanguages}
|
||||
defaultKey={settings.LANGUAGE}
|
||||
onChange={onLanguageChange}
|
||||
tooltip={t(I18nKey.SETTINGS$LANGUAGE_TOOLTIP)}
|
||||
disabled={disabled}
|
||||
/>
|
||||
{enableAdvanced && (
|
||||
<AutocompleteCombobox
|
||||
ariaLabel="agent"
|
||||
items={agents.map((agent) => ({ value: agent, label: agent }))}
|
||||
defaultKey={settings.AGENT}
|
||||
onChange={onAgentChange}
|
||||
tooltip={t(I18nKey.SETTINGS$AGENT_TOOLTIP)}
|
||||
/>
|
||||
)}
|
||||
{enableAdvanced && (
|
||||
<AutocompleteCombobox
|
||||
ariaLabel="securityanalyzer"
|
||||
items={securityAnalyzers.map((securityAnalyzer) => ({
|
||||
value: securityAnalyzer,
|
||||
label: securityAnalyzer,
|
||||
}))}
|
||||
defaultKey={settings.SECURITY_ANALYZER}
|
||||
onChange={onSecurityAnalyzerChange}
|
||||
tooltip={t(I18nKey.SETTINGS$SECURITY_ANALYZER)}
|
||||
disabled={disabled}
|
||||
/>
|
||||
)}
|
||||
{enableAdvanced && (
|
||||
<Switch
|
||||
aria-label="confirmationmode"
|
||||
data-testid="confirmationmode"
|
||||
defaultSelected={
|
||||
settings.CONFIRMATION_MODE || !!settings.SECURITY_ANALYZER
|
||||
}
|
||||
onValueChange={onConfirmationModeChange}
|
||||
isDisabled={disabled || !!settings.SECURITY_ANALYZER}
|
||||
isSelected={settings.CONFIRMATION_MODE}
|
||||
>
|
||||
<Tooltip
|
||||
content={t(I18nKey.SETTINGS$CONFIRMATION_MODE_TOOLTIP)}
|
||||
closeDelay={100}
|
||||
delay={500}
|
||||
>
|
||||
{t(I18nKey.SETTINGS$CONFIRMATION_MODE)}
|
||||
</Tooltip>
|
||||
</Switch>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default SettingsForm;
|
||||
@ -1,202 +0,0 @@
|
||||
import { Spinner } from "@nextui-org/react";
|
||||
import i18next from "i18next";
|
||||
import React, { useEffect } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useSelector } from "react-redux";
|
||||
import { fetchSecurityAnalyzers } from "#/services/options";
|
||||
import { AvailableLanguages } from "#/i18n";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
import Session from "#/services/session";
|
||||
import { RootState } from "../../../store";
|
||||
import AgentState from "../../../types/AgentState";
|
||||
import {
|
||||
Settings,
|
||||
getSettings,
|
||||
getDefaultSettings,
|
||||
settingsAreUpToDate,
|
||||
maybeMigrateSettings,
|
||||
saveSettings,
|
||||
} from "#/services/settings";
|
||||
import toast from "#/utils/toast";
|
||||
import BaseModal from "../base-modal/BaseModal";
|
||||
import SettingsForm from "./SettingsForm";
|
||||
|
||||
interface SettingsProps {
|
||||
isOpen: boolean;
|
||||
onOpenChange: (isOpen: boolean) => void;
|
||||
models: string[];
|
||||
agents: string[];
|
||||
}
|
||||
|
||||
const REQUIRED_SETTINGS = ["LLM_MODEL"];
|
||||
|
||||
function SettingsModal({
|
||||
isOpen,
|
||||
onOpenChange,
|
||||
models,
|
||||
agents,
|
||||
}: SettingsProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [securityAnalyzers, setSecurityAnalyzers] = React.useState<string[]>(
|
||||
[],
|
||||
);
|
||||
const [settings, setSettings] = React.useState<Settings>({} as Settings);
|
||||
const [agentIsRunning, setAgentIsRunning] = React.useState<boolean>(false);
|
||||
const [loading, setLoading] = React.useState(true);
|
||||
const { curAgentState } = useSelector((state: RootState) => state.agent);
|
||||
|
||||
useEffect(() => {
|
||||
maybeMigrateSettings();
|
||||
setSettings(getSettings());
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const isRunning =
|
||||
curAgentState === AgentState.RUNNING ||
|
||||
curAgentState === AgentState.PAUSED ||
|
||||
curAgentState === AgentState.AWAITING_USER_INPUT ||
|
||||
curAgentState === AgentState.AWAITING_USER_CONFIRMATION;
|
||||
setAgentIsRunning(isRunning);
|
||||
}, [curAgentState]);
|
||||
|
||||
React.useEffect(() => {
|
||||
(async () => {
|
||||
try {
|
||||
setSecurityAnalyzers(await fetchSecurityAnalyzers());
|
||||
} catch (error) {
|
||||
toast.error("settings", t(I18nKey.CONFIGURATION$ERROR_FETCH_MODELS));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
})();
|
||||
}, []);
|
||||
|
||||
const handleModelChange = (model: string) => {
|
||||
setSettings((prev) => ({
|
||||
...prev,
|
||||
LLM_MODEL: model,
|
||||
}));
|
||||
};
|
||||
|
||||
const handleBaseURLChange = (baseURL: string) => {
|
||||
setSettings((prev) => ({
|
||||
...prev,
|
||||
LLM_BASE_URL: baseURL,
|
||||
}));
|
||||
};
|
||||
|
||||
const handleAgentChange = (agent: string) => {
|
||||
setSettings((prev) => ({ ...prev, AGENT: agent }));
|
||||
};
|
||||
|
||||
const handleLanguageChange = (language: string) => {
|
||||
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 handleConfirmationModeChange = (confirmationMode: boolean) => {
|
||||
setSettings((prev) => ({ ...prev, CONFIRMATION_MODE: confirmationMode }));
|
||||
};
|
||||
|
||||
const handleSecurityAnalyzerChange = (securityAnalyzer: string) => {
|
||||
setSettings((prev) => ({
|
||||
...prev,
|
||||
CONFIRMATION_MODE: true,
|
||||
SECURITY_ANALYZER: securityAnalyzer,
|
||||
}));
|
||||
};
|
||||
|
||||
const handleResetSettings = () => {
|
||||
setSettings(getDefaultSettings);
|
||||
};
|
||||
|
||||
const handleSaveSettings = () => {
|
||||
saveSettings(settings);
|
||||
i18next.changeLanguage(settings.LANGUAGE);
|
||||
Session.startNewSession();
|
||||
|
||||
localStorage.setItem(
|
||||
`API_KEY_${settings.LLM_MODEL || models[0]}`,
|
||||
settings.LLM_API_KEY,
|
||||
);
|
||||
};
|
||||
|
||||
let subtitle = "";
|
||||
if (loading) {
|
||||
subtitle = t(I18nKey.CONFIGURATION$AGENT_LOADING);
|
||||
} else if (agentIsRunning) {
|
||||
subtitle = t(I18nKey.CONFIGURATION$AGENT_RUNNING);
|
||||
} else if (!settingsAreUpToDate()) {
|
||||
subtitle = t(I18nKey.CONFIGURATION$SETTINGS_NEED_UPDATE_MESSAGE);
|
||||
}
|
||||
const saveIsDisabled = REQUIRED_SETTINGS.some(
|
||||
(key) => !settings[key as keyof Settings],
|
||||
);
|
||||
|
||||
return (
|
||||
<BaseModal
|
||||
isOpen={isOpen}
|
||||
onOpenChange={onOpenChange}
|
||||
title={t(I18nKey.CONFIGURATION$MODAL_TITLE)}
|
||||
isDismissable={settingsAreUpToDate()}
|
||||
subtitle={subtitle}
|
||||
actions={
|
||||
loading
|
||||
? []
|
||||
: [
|
||||
{
|
||||
label: t(I18nKey.CONFIGURATION$MODAL_SAVE_BUTTON_LABEL),
|
||||
action: handleSaveSettings,
|
||||
isDisabled: saveIsDisabled,
|
||||
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: () => {
|
||||
setSettings(getSettings()); // reset settings from any changes
|
||||
},
|
||||
isDisabled: !settingsAreUpToDate(),
|
||||
closeAfterAction: true,
|
||||
className: "bg-rose-600 rounded-lg",
|
||||
},
|
||||
]
|
||||
}
|
||||
>
|
||||
{loading && <Spinner />}
|
||||
{!loading && (
|
||||
<SettingsForm
|
||||
disabled={agentIsRunning}
|
||||
settings={settings}
|
||||
models={models}
|
||||
agents={agents}
|
||||
securityAnalyzers={securityAnalyzers}
|
||||
onModelChange={handleModelChange}
|
||||
onBaseURLChange={handleBaseURLChange}
|
||||
onAgentChange={handleAgentChange}
|
||||
onLanguageChange={handleLanguageChange}
|
||||
onAPIKeyChange={handleAPIKeyChange}
|
||||
onConfirmationModeChange={handleConfirmationModeChange}
|
||||
onSecurityAnalyzerChange={handleSecurityAnalyzerChange}
|
||||
/>
|
||||
)}
|
||||
</BaseModal>
|
||||
);
|
||||
}
|
||||
|
||||
export default SettingsModal;
|
||||
@ -1,21 +1,8 @@
|
||||
import ActionType from "#/types/ActionType";
|
||||
import AgentState from "#/types/AgentState";
|
||||
import Session from "./session";
|
||||
|
||||
const INIT_DELAY = 1000;
|
||||
|
||||
export const generateAgentStateChangeEvent = (state: AgentState) =>
|
||||
JSON.stringify({
|
||||
action: ActionType.CHANGE_AGENT_STATE,
|
||||
args: { agent_state: state },
|
||||
});
|
||||
|
||||
export function changeAgentState(state: AgentState): void {
|
||||
const eventString = generateAgentStateChangeEvent(state);
|
||||
Session.send(eventString);
|
||||
if (state === AgentState.STOPPED) {
|
||||
setTimeout(() => {
|
||||
Session.startNewSession();
|
||||
}, INIT_DELAY);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,221 +0,0 @@
|
||||
import i18next from "i18next";
|
||||
import toast from "#/utils/toast";
|
||||
import { handleAssistantMessage } from "./actions";
|
||||
import { getToken, setToken, clearToken } from "./auth";
|
||||
import ActionType from "#/types/ActionType";
|
||||
import { getSettings } from "./settings";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
|
||||
const translate = (key: I18nKey) => i18next.t(key);
|
||||
|
||||
// Define a type for the messages
|
||||
type Message = {
|
||||
action: ActionType;
|
||||
args: Record<string, unknown>;
|
||||
};
|
||||
|
||||
class Session {
|
||||
private static _socket: WebSocket | null = null;
|
||||
|
||||
private static _latest_event_id: number = -1;
|
||||
|
||||
private static _messageQueue: Message[] = [];
|
||||
|
||||
public static _history: Record<string, unknown>[] = [];
|
||||
|
||||
// callbacks contain a list of callable functions
|
||||
// event: function, like:
|
||||
// open: [function1, function2]
|
||||
// message: [function1, function2]
|
||||
private static callbacks: {
|
||||
[K in keyof WebSocketEventMap]: ((data: WebSocketEventMap[K]) => void)[];
|
||||
} = {
|
||||
open: [],
|
||||
message: [],
|
||||
error: [],
|
||||
close: [],
|
||||
};
|
||||
|
||||
private static _connecting = false;
|
||||
|
||||
private static _disconnecting = false;
|
||||
|
||||
public static restoreOrStartNewSession() {
|
||||
if (Session.isConnected()) {
|
||||
Session.disconnect();
|
||||
}
|
||||
Session._connect();
|
||||
}
|
||||
|
||||
public static startNewSession() {
|
||||
clearToken();
|
||||
Session.restoreOrStartNewSession();
|
||||
}
|
||||
|
||||
private static _initializeAgent = () => {
|
||||
const settings = getSettings();
|
||||
const event = {
|
||||
action: ActionType.INIT,
|
||||
args: {
|
||||
...settings,
|
||||
},
|
||||
};
|
||||
const eventString = JSON.stringify(event);
|
||||
Session.send(eventString);
|
||||
};
|
||||
|
||||
private static _connect(): void {
|
||||
if (Session.isConnected()) return;
|
||||
Session._connecting = true;
|
||||
|
||||
const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
|
||||
let wsURL = `${protocol}//${window.location.host}/ws`;
|
||||
const token = getToken();
|
||||
if (token) {
|
||||
wsURL += `?token=${token}`;
|
||||
if (Session._latest_event_id !== -1) {
|
||||
wsURL += `&latest_event_id=${Session._latest_event_id}`;
|
||||
}
|
||||
}
|
||||
Session._socket = new WebSocket(wsURL);
|
||||
Session._setupSocket();
|
||||
}
|
||||
|
||||
private static _setupSocket(): void {
|
||||
if (!Session._socket) {
|
||||
throw new Error(
|
||||
translate(I18nKey.SESSION$SOCKET_NOT_INITIALIZED_ERROR_MESSAGE),
|
||||
);
|
||||
}
|
||||
Session._socket.onopen = (e) => {
|
||||
toast.success("ws", translate(I18nKey.SESSION$SERVER_CONNECTED_MESSAGE));
|
||||
Session._connecting = false;
|
||||
Session._initializeAgent();
|
||||
Session._flushQueue();
|
||||
Session.callbacks.open?.forEach((callback) => {
|
||||
callback(e);
|
||||
});
|
||||
};
|
||||
|
||||
Session._socket.onmessage = (e) => {
|
||||
let data = null;
|
||||
try {
|
||||
data = JSON.parse(e.data);
|
||||
Session._history.push(data);
|
||||
} catch (err) {
|
||||
toast.error(
|
||||
"ws",
|
||||
translate(I18nKey.SESSION$SESSION_HANDLING_ERROR_MESSAGE),
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (data.error && data.error_code === 401) {
|
||||
Session._latest_event_id = -1;
|
||||
clearToken();
|
||||
} else if (data.token) {
|
||||
setToken(data.token);
|
||||
} else {
|
||||
if (data.id !== undefined) {
|
||||
Session._latest_event_id = data.id;
|
||||
}
|
||||
handleAssistantMessage(data);
|
||||
}
|
||||
};
|
||||
|
||||
Session._socket.onerror = () => {
|
||||
// TODO report error
|
||||
toast.error(
|
||||
"ws",
|
||||
translate(I18nKey.SESSION$SESSION_CONNECTION_ERROR_MESSAGE),
|
||||
);
|
||||
};
|
||||
|
||||
Session._socket.onclose = () => {
|
||||
if (!Session._disconnecting) {
|
||||
setTimeout(() => {
|
||||
Session.restoreOrStartNewSession();
|
||||
}, 3000); // Reconnect after 3 seconds
|
||||
}
|
||||
Session._disconnecting = false;
|
||||
};
|
||||
}
|
||||
|
||||
static isConnected(): boolean {
|
||||
return (
|
||||
Session._socket !== null && Session._socket.readyState === WebSocket.OPEN
|
||||
);
|
||||
}
|
||||
|
||||
static disconnect(): void {
|
||||
Session._disconnecting = true;
|
||||
if (Session._socket) {
|
||||
Session._socket.close();
|
||||
}
|
||||
Session._socket = null;
|
||||
}
|
||||
|
||||
private static _flushQueue(): void {
|
||||
while (Session._messageQueue.length > 0) {
|
||||
const message = Session._messageQueue.shift();
|
||||
if (message) {
|
||||
setTimeout(() => Session.send(JSON.stringify(message)), 1000);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static send(message: string): void {
|
||||
const messageObject: Message = JSON.parse(message);
|
||||
|
||||
if (Session._connecting) {
|
||||
Session._messageQueue.push(messageObject);
|
||||
return;
|
||||
}
|
||||
if (!Session.isConnected()) {
|
||||
throw new Error(
|
||||
translate(I18nKey.SESSION$SESSION_CONNECTION_ERROR_MESSAGE),
|
||||
);
|
||||
}
|
||||
|
||||
if (Session.isConnected()) {
|
||||
Session._socket?.send(message);
|
||||
Session._history.push(JSON.parse(message));
|
||||
} else {
|
||||
toast.error(
|
||||
"ws",
|
||||
translate(I18nKey.SESSION$SESSION_CONNECTION_ERROR_MESSAGE),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
static addEventListener(
|
||||
event: string,
|
||||
callback: (e: MessageEvent) => void,
|
||||
): void {
|
||||
Session._socket?.addEventListener(
|
||||
event as keyof WebSocketEventMap,
|
||||
callback as (
|
||||
this: WebSocket,
|
||||
ev: WebSocketEventMap[keyof WebSocketEventMap],
|
||||
) => never,
|
||||
);
|
||||
}
|
||||
|
||||
static removeEventListener(
|
||||
event: string,
|
||||
listener: (e: Event) => void,
|
||||
): void {
|
||||
Session._socket?.removeEventListener(event, listener);
|
||||
}
|
||||
|
||||
static registerCallback<K extends keyof WebSocketEventMap>(
|
||||
event: K,
|
||||
callbacks: ((data: WebSocketEventMap[K]) => void)[],
|
||||
): void {
|
||||
if (Session.callbacks[event] === undefined) {
|
||||
return;
|
||||
}
|
||||
Session.callbacks[event].push(...callbacks);
|
||||
}
|
||||
}
|
||||
|
||||
export default Session;
|
||||
Loading…
x
Reference in New Issue
Block a user