mirror of
https://github.com/All-Hands-AI/OpenHands.git
synced 2026-04-29 03:00:45 -04:00
Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b1d46cf20f | |||
| 872f5ae8ae | |||
| 0a06af5d17 | |||
| e672a438df | |||
| f1335dab04 | |||
| 1285404349 |
+2
-2
@@ -3,14 +3,14 @@ import { describe, expect, it, vi } from "vitest";
|
||||
import { render, screen, waitFor } from "@testing-library/react";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { AnalyticsConsentFormModal } from "#/components/features/analytics/analytics-consent-form-modal";
|
||||
import OpenHands from "#/api/open-hands";
|
||||
import { AuthProvider } from "#/context/auth-context";
|
||||
import { SettingsService } from "#/api/settings-service/settings-service.api";
|
||||
|
||||
describe("AnalyticsConsentFormModal", () => {
|
||||
it("should call saveUserSettings with consent", async () => {
|
||||
const user = userEvent.setup();
|
||||
const onCloseMock = vi.fn();
|
||||
const saveUserSettingsSpy = vi.spyOn(OpenHands, "saveSettings");
|
||||
const saveUserSettingsSpy = vi.spyOn(SettingsService, "saveSettings");
|
||||
|
||||
render(<AnalyticsConsentFormModal onClose={onCloseMock} />, {
|
||||
wrapper: ({ children }) => (
|
||||
|
||||
@@ -2,7 +2,7 @@ import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { renderWithProviders } from "test-utils";
|
||||
import { createRoutesStub } from "react-router";
|
||||
import { Sidebar } from "#/components/features/sidebar/sidebar";
|
||||
import OpenHands from "#/api/open-hands";
|
||||
import { SettingsService } from "#/api/settings-service/settings-service.api";
|
||||
|
||||
// These tests will now fail because the conversation panel is rendered through a portal
|
||||
// and technically not a child of the Sidebar component.
|
||||
@@ -18,7 +18,7 @@ const renderSidebar = () =>
|
||||
renderWithProviders(<RouterStub initialEntries={["/conversation/123"]} />);
|
||||
|
||||
describe("Sidebar", () => {
|
||||
const getSettingsSpy = vi.spyOn(OpenHands, "getSettings");
|
||||
const getSettingsSpy = vi.spyOn(SettingsService, "getSettings");
|
||||
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks();
|
||||
|
||||
@@ -3,20 +3,20 @@ import { describe, expect, it, vi } from "vitest";
|
||||
import { renderWithProviders } from "test-utils";
|
||||
import { createRoutesStub } from "react-router";
|
||||
import { screen } from "@testing-library/react";
|
||||
import OpenHands from "#/api/open-hands";
|
||||
import { SettingsForm } from "#/components/shared/modals/settings/settings-form";
|
||||
import { DEFAULT_SETTINGS } from "#/services/settings";
|
||||
import { SettingsService } from "#/api/settings-service/settings-service.api";
|
||||
|
||||
describe("SettingsForm", () => {
|
||||
const onCloseMock = vi.fn();
|
||||
const saveSettingsSpy = vi.spyOn(OpenHands, "saveSettings");
|
||||
const saveSettingsSpy = vi.spyOn(SettingsService, "saveSettings");
|
||||
|
||||
const RouteStub = createRoutesStub([
|
||||
{
|
||||
Component: () => (
|
||||
<SettingsForm
|
||||
settings={DEFAULT_SETTINGS}
|
||||
models={[DEFAULT_SETTINGS.LLM_MODEL]}
|
||||
models={[DEFAULT_SETTINGS.llm_model]}
|
||||
onClose={onCloseMock}
|
||||
/>
|
||||
),
|
||||
@@ -33,7 +33,7 @@ describe("SettingsForm", () => {
|
||||
|
||||
expect(saveSettingsSpy).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
llm_model: DEFAULT_SETTINGS.LLM_MODEL,
|
||||
llm_model: DEFAULT_SETTINGS.llm_model,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { renderHook, waitFor } from "@testing-library/react";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import OpenHands from "#/api/open-hands";
|
||||
import { useSaveSettings } from "#/hooks/mutation/use-save-settings";
|
||||
import { AuthProvider } from "#/context/auth-context";
|
||||
import { SettingsService } from "#/api/settings-service/settings-service.api";
|
||||
|
||||
describe("useSaveSettings", () => {
|
||||
it("should send an empty string for llm_api_key if an empty string is passed, otherwise undefined", async () => {
|
||||
const saveSettingsSpy = vi.spyOn(OpenHands, "saveSettings");
|
||||
const saveSettingsSpy = vi.spyOn(SettingsService, "saveSettings");
|
||||
const { result } = renderHook(() => useSaveSettings(), {
|
||||
wrapper: ({ children }) => (
|
||||
<AuthProvider>
|
||||
|
||||
@@ -7,6 +7,7 @@ import MainApp from "#/routes/root-layout";
|
||||
import i18n from "#/i18n";
|
||||
import * as CaptureConsent from "#/utils/handle-capture-consent";
|
||||
import OpenHands from "#/api/open-hands";
|
||||
import { SettingsService } from "#/api/settings-service/settings-service.api";
|
||||
|
||||
describe("frontend/routes/_oh", () => {
|
||||
const RouteStub = createRoutesStub([{ Component: MainApp, path: "/" }]);
|
||||
@@ -59,7 +60,7 @@ describe("frontend/routes/_oh", () => {
|
||||
it.skip("should render and capture the user's consent if oss mode", async () => {
|
||||
const user = userEvent.setup();
|
||||
const getConfigSpy = vi.spyOn(OpenHands, "getConfig");
|
||||
const getSettingsSpy = vi.spyOn(OpenHands, "getSettings");
|
||||
const getSettingsSpy = vi.spyOn(SettingsService, "getSettings");
|
||||
const handleCaptureConsentSpy = vi.spyOn(
|
||||
CaptureConsent,
|
||||
"handleCaptureConsent",
|
||||
|
||||
@@ -8,6 +8,7 @@ import MainApp from "#/routes/root-layout";
|
||||
import SettingsScreen from "#/routes/settings";
|
||||
import Home from "#/routes/home";
|
||||
import OpenHands from "#/api/open-hands";
|
||||
import { SettingsService } from "#/api/settings-service/settings-service.api";
|
||||
|
||||
const createAxiosNotFoundErrorObject = () =>
|
||||
new AxiosError(
|
||||
@@ -25,7 +26,7 @@ const createAxiosNotFoundErrorObject = () =>
|
||||
},
|
||||
);
|
||||
|
||||
const getSettingsSpy = vi.spyOn(OpenHands, "getSettings");
|
||||
const getSettingsSpy = vi.spyOn(SettingsService, "getSettings");
|
||||
|
||||
const RouterStub = createRoutesStub([
|
||||
{
|
||||
|
||||
@@ -7,25 +7,26 @@ import OpenHands from "#/api/open-hands";
|
||||
import { AuthProvider } from "#/context/auth-context";
|
||||
import SettingsScreen from "#/routes/settings";
|
||||
import * as AdvancedSettingsUtlls from "#/utils/has-advanced-settings-set";
|
||||
import { MOCK_DEFAULT_USER_SETTINGS } from "#/mocks/handlers";
|
||||
import * as ConsentHandlers from "#/utils/handle-capture-consent";
|
||||
import AccountSettings from "#/routes/account-settings";
|
||||
import { Provider } from "#/types/settings";
|
||||
import { DEFAULT_SETTINGS } from "#/services/settings";
|
||||
import { SettingsService } from "#/api/settings-service/settings-service.api";
|
||||
import { GitProvider } from "#/api/settings-service/settings-service.types";
|
||||
|
||||
const toggleAdvancedSettings = async (user: UserEvent) => {
|
||||
const advancedSwitch = await screen.findByTestId("advanced-settings-switch");
|
||||
await user.click(advancedSwitch);
|
||||
};
|
||||
|
||||
const mock_provider_tokens_are_set: Record<Provider, boolean> = {
|
||||
const MOCK_PROVIDER_TOKENS_ARE_SET: Record<GitProvider, boolean> = {
|
||||
github: true,
|
||||
gitlab: false,
|
||||
};
|
||||
|
||||
describe("Settings Screen", () => {
|
||||
const getSettingsSpy = vi.spyOn(OpenHands, "getSettings");
|
||||
const saveSettingsSpy = vi.spyOn(OpenHands, "saveSettings");
|
||||
const resetSettingsSpy = vi.spyOn(OpenHands, "resetSettings");
|
||||
const getSettingsSpy = vi.spyOn(SettingsService, "getSettings");
|
||||
const saveSettingsSpy = vi.spyOn(SettingsService, "saveSettings");
|
||||
const resetSettingsSpy = vi.spyOn(SettingsService, "resetSettings");
|
||||
const getConfigSpy = vi.spyOn(OpenHands, "getConfig");
|
||||
|
||||
const { handleLogoutMock } = vi.hoisted(() => ({
|
||||
@@ -65,7 +66,9 @@ describe("Settings Screen", () => {
|
||||
|
||||
await waitFor(() => {
|
||||
// Use queryAllByText to handle multiple elements with the same text
|
||||
expect(screen.queryAllByText("SETTINGS$LLM_SETTINGS")).not.toHaveLength(0);
|
||||
expect(screen.queryAllByText("SETTINGS$LLM_SETTINGS")).not.toHaveLength(
|
||||
0,
|
||||
);
|
||||
screen.getByText("ACCOUNT_SETTINGS$ADDITIONAL_SETTINGS");
|
||||
screen.getByText("BUTTON$RESET_TO_DEFAULTS");
|
||||
screen.getByText("BUTTON$SAVE");
|
||||
@@ -98,9 +101,7 @@ describe("Settings Screen", () => {
|
||||
|
||||
// TODO: Set a better unset indicator
|
||||
it.skip("should render an indicator if the GitHub token is not set", async () => {
|
||||
getSettingsSpy.mockResolvedValue({
|
||||
...MOCK_DEFAULT_USER_SETTINGS,
|
||||
});
|
||||
getSettingsSpy.mockResolvedValue(DEFAULT_SETTINGS);
|
||||
|
||||
renderSettingsScreen();
|
||||
|
||||
@@ -119,8 +120,8 @@ describe("Settings Screen", () => {
|
||||
|
||||
it("should set '<hidden>' placeholder if the GitHub token is set", async () => {
|
||||
getSettingsSpy.mockResolvedValue({
|
||||
...MOCK_DEFAULT_USER_SETTINGS,
|
||||
provider_tokens_set: mock_provider_tokens_are_set,
|
||||
...DEFAULT_SETTINGS,
|
||||
provider_tokens_set: MOCK_PROVIDER_TOKENS_ARE_SET,
|
||||
});
|
||||
|
||||
renderSettingsScreen();
|
||||
@@ -133,8 +134,8 @@ describe("Settings Screen", () => {
|
||||
|
||||
it("should render an indicator if the GitHub token is set", async () => {
|
||||
getSettingsSpy.mockResolvedValue({
|
||||
...MOCK_DEFAULT_USER_SETTINGS,
|
||||
provider_tokens_set: mock_provider_tokens_are_set,
|
||||
...DEFAULT_SETTINGS,
|
||||
provider_tokens_set: MOCK_PROVIDER_TOKENS_ARE_SET,
|
||||
});
|
||||
|
||||
renderSettingsScreen();
|
||||
@@ -150,8 +151,6 @@ describe("Settings Screen", () => {
|
||||
}
|
||||
});
|
||||
|
||||
// Tests for DISCONNECT_FROM_GITHUB button removed as the button is no longer included in main
|
||||
|
||||
it("should not render the 'Configure GitHub Repositories' button if OSS mode", async () => {
|
||||
getConfigSpy.mockResolvedValue({
|
||||
APP_MODE: "oss",
|
||||
@@ -210,7 +209,7 @@ describe("Settings Screen", () => {
|
||||
it.skip("should not reset LLM Provider and Model if GitHub token is invalid", async () => {
|
||||
const user = userEvent.setup();
|
||||
getSettingsSpy.mockResolvedValue({
|
||||
...MOCK_DEFAULT_USER_SETTINGS,
|
||||
...DEFAULT_SETTINGS,
|
||||
llm_model: "anthropic/claude-3-5-sonnet-20241022",
|
||||
});
|
||||
saveSettingsSpy.mockRejectedValueOnce(new Error("Invalid GitHub token"));
|
||||
@@ -331,8 +330,8 @@ describe("Settings Screen", () => {
|
||||
// TODO: Set a better unset indicator
|
||||
it.skip("should render an indicator if the LLM API key is not set", async () => {
|
||||
getSettingsSpy.mockResolvedValueOnce({
|
||||
...MOCK_DEFAULT_USER_SETTINGS,
|
||||
llm_api_key: null,
|
||||
...DEFAULT_SETTINGS,
|
||||
llm_api_key_set: null,
|
||||
});
|
||||
|
||||
renderSettingsScreen();
|
||||
@@ -352,7 +351,7 @@ describe("Settings Screen", () => {
|
||||
|
||||
it("should render an indicator if the LLM API key is set", async () => {
|
||||
getSettingsSpy.mockResolvedValueOnce({
|
||||
...MOCK_DEFAULT_USER_SETTINGS,
|
||||
...DEFAULT_SETTINGS,
|
||||
llm_api_key_set: true,
|
||||
});
|
||||
|
||||
@@ -373,7 +372,7 @@ describe("Settings Screen", () => {
|
||||
|
||||
it("should set '<hidden>' placeholder if the LLM API key is set", async () => {
|
||||
getSettingsSpy.mockResolvedValueOnce({
|
||||
...MOCK_DEFAULT_USER_SETTINGS,
|
||||
...DEFAULT_SETTINGS,
|
||||
llm_api_key_set: true,
|
||||
});
|
||||
|
||||
@@ -388,7 +387,7 @@ describe("Settings Screen", () => {
|
||||
describe("Basic Model Selector", () => {
|
||||
it("should set the provider and model", async () => {
|
||||
getSettingsSpy.mockResolvedValue({
|
||||
...MOCK_DEFAULT_USER_SETTINGS,
|
||||
...DEFAULT_SETTINGS,
|
||||
llm_model: "anthropic/claude-3-5-sonnet-20241022",
|
||||
});
|
||||
|
||||
@@ -455,7 +454,7 @@ describe("Settings Screen", () => {
|
||||
});
|
||||
|
||||
getSettingsSpy.mockResolvedValue({
|
||||
...MOCK_DEFAULT_USER_SETTINGS,
|
||||
...DEFAULT_SETTINGS,
|
||||
remote_runtime_resource_factor: 1,
|
||||
});
|
||||
|
||||
@@ -495,7 +494,7 @@ describe("Settings Screen", () => {
|
||||
});
|
||||
|
||||
getSettingsSpy.mockResolvedValue({
|
||||
...MOCK_DEFAULT_USER_SETTINGS,
|
||||
...DEFAULT_SETTINGS,
|
||||
});
|
||||
|
||||
renderSettingsScreen();
|
||||
@@ -546,7 +545,7 @@ describe("Settings Screen", () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
getSettingsSpy.mockResolvedValueOnce({
|
||||
...MOCK_DEFAULT_USER_SETTINGS,
|
||||
...DEFAULT_SETTINGS,
|
||||
});
|
||||
|
||||
renderSettingsScreen();
|
||||
@@ -563,7 +562,7 @@ describe("Settings Screen", () => {
|
||||
// Mock the settings that will be returned after reset
|
||||
// This should be the default settings with no advanced settings enabled
|
||||
getSettingsSpy.mockResolvedValueOnce({
|
||||
...MOCK_DEFAULT_USER_SETTINGS,
|
||||
...DEFAULT_SETTINGS,
|
||||
llm_base_url: "",
|
||||
confirmation_mode: false,
|
||||
security_analyzer: "",
|
||||
@@ -614,7 +613,7 @@ describe("Settings Screen", () => {
|
||||
|
||||
it("should toggle advanced if user had set a custom model", async () => {
|
||||
getSettingsSpy.mockResolvedValue({
|
||||
...MOCK_DEFAULT_USER_SETTINGS,
|
||||
...DEFAULT_SETTINGS,
|
||||
llm_model: "some/custom-model",
|
||||
});
|
||||
renderSettingsScreen();
|
||||
@@ -649,7 +648,7 @@ describe("Settings Screen", () => {
|
||||
|
||||
it("should have confirmation mode enabled if the user previously had it enabled", async () => {
|
||||
getSettingsSpy.mockResolvedValue({
|
||||
...MOCK_DEFAULT_USER_SETTINGS,
|
||||
...DEFAULT_SETTINGS,
|
||||
confirmation_mode: true,
|
||||
});
|
||||
|
||||
@@ -666,7 +665,7 @@ describe("Settings Screen", () => {
|
||||
// FIXME: security analyzer is not found for some reason...
|
||||
it.skip("should have the values set if the user previously had them set", async () => {
|
||||
getSettingsSpy.mockResolvedValue({
|
||||
...MOCK_DEFAULT_USER_SETTINGS,
|
||||
...DEFAULT_SETTINGS,
|
||||
language: "no",
|
||||
user_consents_to_analytics: true,
|
||||
llm_base_url: "https://test.com",
|
||||
@@ -701,7 +700,7 @@ describe("Settings Screen", () => {
|
||||
it("should save the settings when the 'Save Changes' button is clicked", async () => {
|
||||
const user = userEvent.setup();
|
||||
getSettingsSpy.mockResolvedValue({
|
||||
...MOCK_DEFAULT_USER_SETTINGS,
|
||||
...DEFAULT_SETTINGS,
|
||||
});
|
||||
|
||||
renderSettingsScreen();
|
||||
@@ -728,7 +727,7 @@ describe("Settings Screen", () => {
|
||||
it("should properly save basic LLM model settings", async () => {
|
||||
const user = userEvent.setup();
|
||||
getSettingsSpy.mockResolvedValue({
|
||||
...MOCK_DEFAULT_USER_SETTINGS,
|
||||
...DEFAULT_SETTINGS,
|
||||
});
|
||||
|
||||
renderSettingsScreen();
|
||||
@@ -764,7 +763,7 @@ describe("Settings Screen", () => {
|
||||
|
||||
it("should reset the settings when the 'Reset to defaults' button is clicked", async () => {
|
||||
const user = userEvent.setup();
|
||||
getSettingsSpy.mockResolvedValue(MOCK_DEFAULT_USER_SETTINGS);
|
||||
getSettingsSpy.mockResolvedValue(DEFAULT_SETTINGS);
|
||||
|
||||
renderSettingsScreen();
|
||||
|
||||
@@ -795,7 +794,7 @@ describe("Settings Screen", () => {
|
||||
|
||||
// Mock the settings response after reset
|
||||
getSettingsSpy.mockResolvedValueOnce({
|
||||
...MOCK_DEFAULT_USER_SETTINGS,
|
||||
...DEFAULT_SETTINGS,
|
||||
llm_base_url: "",
|
||||
confirmation_mode: false,
|
||||
security_analyzer: "",
|
||||
@@ -820,7 +819,7 @@ describe("Settings Screen", () => {
|
||||
|
||||
it("should cancel the reset when the 'Cancel' button is clicked", async () => {
|
||||
const user = userEvent.setup();
|
||||
getSettingsSpy.mockResolvedValue(MOCK_DEFAULT_USER_SETTINGS);
|
||||
getSettingsSpy.mockResolvedValue(DEFAULT_SETTINGS);
|
||||
|
||||
renderSettingsScreen();
|
||||
|
||||
@@ -928,7 +927,7 @@ describe("Settings Screen", () => {
|
||||
it("should not send an empty LLM API Key if the user submits an empty string but already has it set", async () => {
|
||||
const user = userEvent.setup();
|
||||
getSettingsSpy.mockResolvedValue({
|
||||
...MOCK_DEFAULT_USER_SETTINGS,
|
||||
...DEFAULT_SETTINGS,
|
||||
llm_api_key_set: true,
|
||||
});
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ describe("hasAdvancedSettingsSet", () => {
|
||||
expect(
|
||||
hasAdvancedSettingsSet({
|
||||
...DEFAULT_SETTINGS,
|
||||
LLM_BASE_URL: "test",
|
||||
llm_base_url: "test",
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
@@ -21,7 +21,7 @@ describe("hasAdvancedSettingsSet", () => {
|
||||
expect(
|
||||
hasAdvancedSettingsSet({
|
||||
...DEFAULT_SETTINGS,
|
||||
AGENT: "test",
|
||||
agent: "test",
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
@@ -30,7 +30,7 @@ describe("hasAdvancedSettingsSet", () => {
|
||||
expect(
|
||||
hasAdvancedSettingsSet({
|
||||
...DEFAULT_SETTINGS,
|
||||
REMOTE_RUNTIME_RESOURCE_FACTOR: 999,
|
||||
remote_runtime_resource_factor: 999,
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
@@ -39,7 +39,7 @@ describe("hasAdvancedSettingsSet", () => {
|
||||
expect(
|
||||
hasAdvancedSettingsSet({
|
||||
...DEFAULT_SETTINGS,
|
||||
CONFIRMATION_MODE: true,
|
||||
confirmation_mode: true,
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
@@ -48,7 +48,7 @@ describe("hasAdvancedSettingsSet", () => {
|
||||
expect(
|
||||
hasAdvancedSettingsSet({
|
||||
...DEFAULT_SETTINGS,
|
||||
SECURITY_ANALYZER: "test",
|
||||
security_analyzer: "test",
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
@@ -10,7 +10,6 @@ import {
|
||||
GetTrajectoryResponse,
|
||||
} from "./open-hands.types";
|
||||
import { openHands } from "./open-hands-axios";
|
||||
import { ApiSettings, PostApiSettings } from "#/types/settings";
|
||||
import { GitUser, GitRepository } from "#/types/git";
|
||||
|
||||
class OpenHands {
|
||||
@@ -178,33 +177,6 @@ class OpenHands {
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the settings from the server or use the default settings if not found
|
||||
*/
|
||||
static async getSettings(): Promise<ApiSettings> {
|
||||
const { data } = await openHands.get<ApiSettings>("/api/settings");
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Save the settings to the server. Only valid settings are saved.
|
||||
* @param settings - the settings to save
|
||||
*/
|
||||
static async saveSettings(
|
||||
settings: Partial<PostApiSettings>,
|
||||
): Promise<boolean> {
|
||||
const data = await openHands.post("/api/settings", settings);
|
||||
return data.status === 200;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset user settings in server
|
||||
*/
|
||||
static async resetSettings(): Promise<boolean> {
|
||||
const response = await openHands.post("/api/reset-settings");
|
||||
return response.status === 200;
|
||||
}
|
||||
|
||||
static async createCheckoutSession(amount: number): Promise<string> {
|
||||
const { data } = await openHands.post(
|
||||
"/api/billing/create-checkout-session",
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import { openHands } from "../open-hands-axios";
|
||||
import { UserSettings } from "./settings-service.types";
|
||||
|
||||
export class SettingsService {
|
||||
/**
|
||||
* Get the user's settings
|
||||
*/
|
||||
static async getSettings(): Promise<UserSettings> {
|
||||
const { data } = await openHands.get<UserSettings>("/api/settings");
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Save valid settings to the server
|
||||
* @param settings - The settings to save
|
||||
*/
|
||||
static async saveSettings(settings: Partial<UserSettings>): Promise<boolean> {
|
||||
const data = await openHands.post("/api/settings", settings);
|
||||
return data.status === 200;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset the user's settings
|
||||
*/
|
||||
static async resetSettings(): Promise<boolean> {
|
||||
const response = await openHands.post("/api/settings/reset");
|
||||
return response.status === 200;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
export type GitProvider = "github" | "gitlab";
|
||||
|
||||
export interface UserSettings {
|
||||
llm_model: string;
|
||||
llm_base_url: string;
|
||||
agent: string;
|
||||
language: string;
|
||||
llm_api_key_set: boolean;
|
||||
confirmation_mode: boolean;
|
||||
security_analyzer: string;
|
||||
remote_runtime_resource_factor: number | null;
|
||||
github_token_is_set: boolean;
|
||||
enable_default_condenser: boolean;
|
||||
enable_sound_notifications: boolean;
|
||||
user_consents_to_analytics: boolean | null;
|
||||
provider_tokens_set: Record<GitProvider, boolean>;
|
||||
}
|
||||
|
||||
// These are settings that are only used on the client side and should not be sent to the server
|
||||
export interface ClientUserSettings extends UserSettings {
|
||||
is_new_user: boolean;
|
||||
}
|
||||
|
||||
// These are settings that are used on the server side and should be sent to the server
|
||||
export interface ServerUserSettings extends UserSettings {
|
||||
github_token: string | null;
|
||||
}
|
||||
@@ -9,15 +9,16 @@ import { extractSettings } from "#/utils/settings-utils";
|
||||
import { useEndSession } from "#/hooks/use-end-session";
|
||||
import { ModalBackdrop } from "../modal-backdrop";
|
||||
import { ModelSelector } from "./model-selector";
|
||||
import { Settings } from "#/types/settings";
|
||||
import { BrandButton } from "#/components/features/settings/brand-button";
|
||||
import { KeyStatusIcon } from "#/components/features/settings/key-status-icon";
|
||||
import { SettingsInput } from "#/components/features/settings/settings-input";
|
||||
import { HelpLink } from "#/components/features/settings/help-link";
|
||||
import { useSaveSettings } from "#/hooks/mutation/use-save-settings";
|
||||
import { UserSettings } from "#/api/settings-service/settings-service.types";
|
||||
import { DEFAULT_SETTINGS } from "#/services/settings";
|
||||
|
||||
interface SettingsFormProps {
|
||||
settings: Settings;
|
||||
settings: UserSettings | undefined;
|
||||
models: string[];
|
||||
onClose: () => void;
|
||||
}
|
||||
@@ -42,17 +43,16 @@ export function SettingsForm({ settings, models, onClose }: SettingsFormProps) {
|
||||
|
||||
const handleFormSubmission = async (formData: FormData) => {
|
||||
const newSettings = extractSettings(formData);
|
||||
|
||||
await saveUserSettings(newSettings, {
|
||||
saveUserSettings(newSettings, {
|
||||
onSuccess: () => {
|
||||
onClose();
|
||||
resetOngoingSession();
|
||||
|
||||
posthog.capture("settings_saved", {
|
||||
LLM_MODEL: newSettings.LLM_MODEL,
|
||||
LLM_API_KEY_SET: newSettings.LLM_API_KEY_SET ? "SET" : "UNSET",
|
||||
LLM_MODEL: newSettings.llm_model,
|
||||
LLM_API_KEY: newSettings.llm_api_key_set ? "SET" : "UNSET",
|
||||
REMOTE_RUNTIME_RESOURCE_FACTOR:
|
||||
newSettings.REMOTE_RUNTIME_RESOURCE_FACTOR,
|
||||
newSettings.remote_runtime_resource_factor,
|
||||
});
|
||||
},
|
||||
});
|
||||
@@ -74,7 +74,7 @@ export function SettingsForm({ settings, models, onClose }: SettingsFormProps) {
|
||||
}
|
||||
};
|
||||
|
||||
const isLLMKeySet = settings.LLM_API_KEY_SET;
|
||||
const isLLMKeySet = !!settings?.llm_api_key_set;
|
||||
|
||||
return (
|
||||
<div>
|
||||
@@ -87,7 +87,7 @@ export function SettingsForm({ settings, models, onClose }: SettingsFormProps) {
|
||||
<div className="flex flex-col gap-4">
|
||||
<ModelSelector
|
||||
models={organizeModelsAndProviders(models)}
|
||||
currentModel={settings.LLM_MODEL}
|
||||
currentModel={settings?.llm_model || DEFAULT_SETTINGS.llm_model}
|
||||
/>
|
||||
|
||||
<SettingsInput
|
||||
|
||||
@@ -5,11 +5,10 @@ import { I18nKey } from "#/i18n/declaration";
|
||||
import { LoadingSpinner } from "../../loading-spinner";
|
||||
import { ModalBackdrop } from "../modal-backdrop";
|
||||
import { SettingsForm } from "./settings-form";
|
||||
import { Settings } from "#/types/settings";
|
||||
import { DEFAULT_SETTINGS } from "#/services/settings";
|
||||
import { UserSettings } from "#/api/settings-service/settings-service.types";
|
||||
|
||||
interface SettingsModalProps {
|
||||
settings?: Settings;
|
||||
settings: UserSettings | undefined;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
@@ -47,7 +46,7 @@ export function SettingsModal({ onClose, settings }: SettingsModalProps) {
|
||||
)}
|
||||
{aiConfigOptions.data && (
|
||||
<SettingsForm
|
||||
settings={settings || DEFAULT_SETTINGS}
|
||||
settings={settings}
|
||||
models={aiConfigOptions.data?.models}
|
||||
onClose={onClose}
|
||||
/>
|
||||
|
||||
@@ -1,37 +1,29 @@
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { DEFAULT_SETTINGS } from "#/services/settings";
|
||||
import OpenHands from "#/api/open-hands";
|
||||
import { PostSettings, PostApiSettings } from "#/types/settings";
|
||||
import { useSettings } from "../query/use-settings";
|
||||
import { UserSettings } from "#/api/settings-service/settings-service.types";
|
||||
import { SettingsService } from "#/api/settings-service/settings-service.api";
|
||||
|
||||
const saveSettingsMutationFn = async (
|
||||
settings: Partial<PostSettings> | null,
|
||||
settings: Partial<UserSettings> | null,
|
||||
) => {
|
||||
// If settings is null, we're resetting
|
||||
if (settings === null) {
|
||||
await OpenHands.resetSettings();
|
||||
await SettingsService.resetSettings();
|
||||
return;
|
||||
}
|
||||
|
||||
const apiSettings: Partial<PostApiSettings> = {
|
||||
llm_model: settings.LLM_MODEL,
|
||||
llm_base_url: settings.LLM_BASE_URL,
|
||||
agent: settings.AGENT || DEFAULT_SETTINGS.AGENT,
|
||||
language: settings.LANGUAGE || DEFAULT_SETTINGS.LANGUAGE,
|
||||
confirmation_mode: settings.CONFIRMATION_MODE,
|
||||
security_analyzer: settings.SECURITY_ANALYZER,
|
||||
llm_api_key:
|
||||
settings.llm_api_key === ""
|
||||
const safeSettings: Partial<UserSettings> = {
|
||||
...settings,
|
||||
agent: settings.agent || DEFAULT_SETTINGS.agent,
|
||||
language: settings.language || DEFAULT_SETTINGS.language,
|
||||
llm_api_key_set:
|
||||
settings.llm_api_key_set === ""
|
||||
? ""
|
||||
: settings.llm_api_key?.trim() || undefined,
|
||||
remote_runtime_resource_factor: settings.REMOTE_RUNTIME_RESOURCE_FACTOR,
|
||||
enable_default_condenser: settings.ENABLE_DEFAULT_CONDENSER,
|
||||
enable_sound_notifications: settings.ENABLE_SOUND_NOTIFICATIONS,
|
||||
user_consents_to_analytics: settings.user_consents_to_analytics,
|
||||
provider_tokens: settings.provider_tokens,
|
||||
: settings.llm_api_key_set?.trim() || undefined,
|
||||
};
|
||||
|
||||
await OpenHands.saveSettings(apiSettings);
|
||||
await SettingsService.saveSettings(safeSettings);
|
||||
};
|
||||
|
||||
export const useSaveSettings = () => {
|
||||
@@ -39,7 +31,7 @@ export const useSaveSettings = () => {
|
||||
const { data: currentSettings } = useSettings();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async (settings: Partial<PostSettings> | null) => {
|
||||
mutationFn: async (settings: Partial<UserSettings> | null) => {
|
||||
if (settings === null) {
|
||||
await saveSettingsMutationFn(null);
|
||||
return;
|
||||
|
||||
@@ -1,29 +1,14 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import React from "react";
|
||||
import posthog from "posthog-js";
|
||||
import OpenHands from "#/api/open-hands";
|
||||
import { useAuth } from "#/context/auth-context";
|
||||
import { SettingsService } from "#/api/settings-service/settings-service.api";
|
||||
import { ClientUserSettings } from "#/api/settings-service/settings-service.types";
|
||||
import { DEFAULT_SETTINGS } from "#/services/settings";
|
||||
|
||||
const getSettingsQueryFn = async () => {
|
||||
const apiSettings = await OpenHands.getSettings();
|
||||
|
||||
return {
|
||||
LLM_MODEL: apiSettings.llm_model,
|
||||
LLM_BASE_URL: apiSettings.llm_base_url,
|
||||
AGENT: apiSettings.agent,
|
||||
LANGUAGE: apiSettings.language,
|
||||
CONFIRMATION_MODE: apiSettings.confirmation_mode,
|
||||
SECURITY_ANALYZER: apiSettings.security_analyzer,
|
||||
LLM_API_KEY_SET: apiSettings.llm_api_key_set,
|
||||
REMOTE_RUNTIME_RESOURCE_FACTOR: apiSettings.remote_runtime_resource_factor,
|
||||
PROVIDER_TOKENS_SET: apiSettings.provider_tokens_set,
|
||||
ENABLE_DEFAULT_CONDENSER: apiSettings.enable_default_condenser,
|
||||
ENABLE_SOUND_NOTIFICATIONS: apiSettings.enable_sound_notifications,
|
||||
USER_CONSENTS_TO_ANALYTICS: apiSettings.user_consents_to_analytics,
|
||||
PROVIDER_TOKENS: apiSettings.provider_tokens,
|
||||
IS_NEW_USER: false,
|
||||
};
|
||||
const settingsQueryFn = async (): Promise<ClientUserSettings> => {
|
||||
const settings = await SettingsService.getSettings();
|
||||
return { ...settings, is_new_user: false };
|
||||
};
|
||||
|
||||
export const useSettings = () => {
|
||||
@@ -32,7 +17,7 @@ export const useSettings = () => {
|
||||
|
||||
const query = useQuery({
|
||||
queryKey: ["settings", providerTokensSet],
|
||||
queryFn: getSettingsQueryFn,
|
||||
queryFn: settingsQueryFn,
|
||||
// Only retry if the error is not a 404 because we
|
||||
// would want to show the modal immediately if the
|
||||
// settings are not found
|
||||
@@ -45,42 +30,36 @@ export const useSettings = () => {
|
||||
});
|
||||
|
||||
React.useEffect(() => {
|
||||
if (query.isFetched && query.data?.LLM_API_KEY_SET) {
|
||||
if (query.isFetched && query.data?.llm_api_key_set) {
|
||||
posthog.capture("user_activated");
|
||||
}
|
||||
}, [query.data?.LLM_API_KEY_SET, query.isFetched]);
|
||||
}, [query.data?.llm_api_key_set, query.isFetched]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (query.data?.PROVIDER_TOKENS_SET) {
|
||||
const providers = query.data.PROVIDER_TOKENS_SET;
|
||||
if (query.isFetched && query.data?.provider_tokens_set) {
|
||||
const providers = query.data.provider_tokens_set;
|
||||
const setProviders = (
|
||||
Object.keys(providers) as Array<keyof typeof providers>
|
||||
).filter((key) => providers[key]);
|
||||
setProviderTokensSet(setProviders);
|
||||
const atLeastOneSet = Object.values(query.data.PROVIDER_TOKENS_SET).some(
|
||||
const atLeastOneSet = Object.values(query.data.provider_tokens_set).some(
|
||||
(value) => value,
|
||||
);
|
||||
setProvidersAreSet(atLeastOneSet);
|
||||
}
|
||||
}, [query.data?.PROVIDER_TOKENS_SET, query.isFetched]);
|
||||
}, [query.data?.provider_tokens_set, query.isFetched]);
|
||||
|
||||
// We want to return the defaults if the settings aren't found so the user can still see the
|
||||
// options to make their initial save. We don't set the defaults in `initialData` above because
|
||||
// that would prepopulate the data to the cache and mess with expectations. Read more:
|
||||
// https://tanstack.com/query/latest/docs/framework/react/guides/initial-query-data#using-initialdata-to-prepopulate-a-query
|
||||
if (query.error?.status === 404) {
|
||||
// Create a new object with only the properties we need, avoiding rest destructuring
|
||||
// Object rest destructuring on a query will observe all changes to the query, leading to excessive re-renders.
|
||||
// Only return the specific properties we need to avoid this.
|
||||
return {
|
||||
data: DEFAULT_SETTINGS,
|
||||
error: query.error,
|
||||
isError: query.isError,
|
||||
isLoading: query.isLoading,
|
||||
isFetching: query.isFetching,
|
||||
isFetched: query.isFetched,
|
||||
isFetching: query.isFetching,
|
||||
isSuccess: query.isSuccess,
|
||||
status: query.status,
|
||||
fetchStatus: query.fetchStatus,
|
||||
refetch: query.refetch,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ export const useMigrateUserConsent = () => {
|
||||
if (userAnalyticsConsent) {
|
||||
args?.handleAnalyticsWasPresentInLocalStorage();
|
||||
|
||||
await saveUserSettings(
|
||||
saveUserSettings(
|
||||
{ user_consents_to_analytics: userAnalyticsConsent === "true" },
|
||||
{
|
||||
onSuccess: () => {
|
||||
|
||||
@@ -26,7 +26,7 @@ export const useNotification = () => {
|
||||
// 4. Not a settings-related notification
|
||||
if (
|
||||
options?.playSound === true && // Must be explicitly true
|
||||
settings?.ENABLE_SOUND_NOTIFICATIONS &&
|
||||
settings?.enable_sound_notifications &&
|
||||
audioRef.current &&
|
||||
!title.includes("BUTTON$") // Don't play for button/settings actions
|
||||
) {
|
||||
@@ -49,7 +49,7 @@ export const useNotification = () => {
|
||||
|
||||
return undefined;
|
||||
},
|
||||
[settings?.ENABLE_SOUND_NOTIFICATIONS],
|
||||
[settings?.enable_sound_notifications],
|
||||
);
|
||||
|
||||
return { notify };
|
||||
|
||||
@@ -4,36 +4,11 @@ import {
|
||||
Conversation,
|
||||
ResultSet,
|
||||
} from "#/api/open-hands.types";
|
||||
import { DEFAULT_SETTINGS } from "#/services/settings";
|
||||
import { STRIPE_BILLING_HANDLERS } from "./billing-handlers";
|
||||
import { ApiSettings, PostApiSettings } from "#/types/settings";
|
||||
import { SETTINGS_HANDLERS } from "./settings-handlers";
|
||||
import { FILE_SERVICE_HANDLERS } from "./file-service-handlers";
|
||||
import { GitUser } from "#/types/git";
|
||||
|
||||
export const MOCK_DEFAULT_USER_SETTINGS: ApiSettings | PostApiSettings = {
|
||||
llm_model: DEFAULT_SETTINGS.LLM_MODEL,
|
||||
llm_base_url: DEFAULT_SETTINGS.LLM_BASE_URL,
|
||||
llm_api_key: null,
|
||||
llm_api_key_set: DEFAULT_SETTINGS.LLM_API_KEY_SET,
|
||||
agent: DEFAULT_SETTINGS.AGENT,
|
||||
language: DEFAULT_SETTINGS.LANGUAGE,
|
||||
confirmation_mode: DEFAULT_SETTINGS.CONFIRMATION_MODE,
|
||||
security_analyzer: DEFAULT_SETTINGS.SECURITY_ANALYZER,
|
||||
remote_runtime_resource_factor:
|
||||
DEFAULT_SETTINGS.REMOTE_RUNTIME_RESOURCE_FACTOR,
|
||||
provider_tokens_set: DEFAULT_SETTINGS.PROVIDER_TOKENS_SET,
|
||||
enable_default_condenser: DEFAULT_SETTINGS.ENABLE_DEFAULT_CONDENSER,
|
||||
enable_sound_notifications: DEFAULT_SETTINGS.ENABLE_SOUND_NOTIFICATIONS,
|
||||
user_consents_to_analytics: DEFAULT_SETTINGS.USER_CONSENTS_TO_ANALYTICS,
|
||||
provider_tokens: DEFAULT_SETTINGS.PROVIDER_TOKENS,
|
||||
};
|
||||
|
||||
const MOCK_USER_PREFERENCES: {
|
||||
settings: ApiSettings | PostApiSettings | null;
|
||||
} = {
|
||||
settings: null,
|
||||
};
|
||||
|
||||
const conversations: Conversation[] = [
|
||||
{
|
||||
conversation_id: "1",
|
||||
@@ -104,6 +79,7 @@ const openHandsHandlers = [
|
||||
|
||||
export const handlers = [
|
||||
...STRIPE_BILLING_HANDLERS,
|
||||
...SETTINGS_HANDLERS,
|
||||
...FILE_SERVICE_HANDLERS,
|
||||
...openHandsHandlers,
|
||||
http.get("/api/user/repositories", () =>
|
||||
@@ -146,40 +122,6 @@ export const handlers = [
|
||||
|
||||
return HttpResponse.json(config);
|
||||
}),
|
||||
http.get("/api/settings", async () => {
|
||||
await delay();
|
||||
|
||||
const { settings } = MOCK_USER_PREFERENCES;
|
||||
|
||||
if (!settings) return HttpResponse.json(null, { status: 404 });
|
||||
|
||||
if (Object.keys(settings.provider_tokens_set).length > 0)
|
||||
settings.provider_tokens_set = { github: false, gitlab: false };
|
||||
|
||||
return HttpResponse.json(settings);
|
||||
}),
|
||||
http.post("/api/settings", async ({ request }) => {
|
||||
const body = await request.json();
|
||||
|
||||
if (body) {
|
||||
let newSettings: Partial<PostApiSettings> = {};
|
||||
if (typeof body === "object") {
|
||||
newSettings = { ...body };
|
||||
}
|
||||
|
||||
const fullSettings = {
|
||||
...MOCK_DEFAULT_USER_SETTINGS,
|
||||
...MOCK_USER_PREFERENCES.settings,
|
||||
...newSettings,
|
||||
};
|
||||
|
||||
MOCK_USER_PREFERENCES.settings = fullSettings;
|
||||
return HttpResponse.json(null, { status: 200 });
|
||||
}
|
||||
|
||||
return HttpResponse.json(null, { status: 400 });
|
||||
}),
|
||||
|
||||
http.post("/api/authenticate", async () =>
|
||||
HttpResponse.json({ message: "Authenticated" }),
|
||||
),
|
||||
@@ -260,10 +202,4 @@ export const handlers = [
|
||||
}),
|
||||
|
||||
http.post("/api/logout", () => HttpResponse.json(null, { status: 200 })),
|
||||
|
||||
http.post("/api/reset-settings", async () => {
|
||||
await delay();
|
||||
MOCK_USER_PREFERENCES.settings = { ...MOCK_DEFAULT_USER_SETTINGS };
|
||||
return HttpResponse.json(null, { status: 200 });
|
||||
}),
|
||||
];
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
import { delay, http, HttpResponse } from "msw";
|
||||
import { UserSettings } from "#/api/settings-service/settings-service.types";
|
||||
import { DEFAULT_SETTINGS } from "#/services/settings";
|
||||
|
||||
const MOCK_USER_PREFERENCES: {
|
||||
settings: UserSettings | null;
|
||||
} = {
|
||||
settings: null,
|
||||
};
|
||||
|
||||
export const SETTINGS_HANDLERS = [
|
||||
http.get("/api/settings", async () => {
|
||||
await delay();
|
||||
const { settings } = MOCK_USER_PREFERENCES;
|
||||
|
||||
if (!settings) return HttpResponse.json(null, { status: 404 });
|
||||
|
||||
if (Object.keys(settings.provider_tokens_set).length > 0)
|
||||
settings.github_token_is_set = true;
|
||||
|
||||
return HttpResponse.json(settings);
|
||||
}),
|
||||
|
||||
http.post("/api/settings", async ({ request }) => {
|
||||
const body = await request.json();
|
||||
|
||||
if (body) {
|
||||
let newSettings: Partial<UserSettings> = {};
|
||||
|
||||
if (typeof body === "object") {
|
||||
newSettings = { ...body };
|
||||
}
|
||||
|
||||
const fullSettings: UserSettings = {
|
||||
...DEFAULT_SETTINGS,
|
||||
...MOCK_USER_PREFERENCES.settings,
|
||||
...newSettings,
|
||||
};
|
||||
|
||||
MOCK_USER_PREFERENCES.settings = fullSettings;
|
||||
return HttpResponse.json(null, { status: 200 });
|
||||
}
|
||||
|
||||
return HttpResponse.json(null, { status: 400 });
|
||||
}),
|
||||
|
||||
http.post("/api/settings/reset", async () => {
|
||||
await delay();
|
||||
MOCK_USER_PREFERENCES.settings = { ...DEFAULT_SETTINGS };
|
||||
return HttpResponse.json(null, { status: 200 });
|
||||
}),
|
||||
];
|
||||
@@ -27,6 +27,7 @@ import {
|
||||
displayErrorToast,
|
||||
displaySuccessToast,
|
||||
} from "#/utils/custom-toast-handlers";
|
||||
import { ServerUserSettings } from "#/api/settings-service/settings-service.types";
|
||||
import { ProviderOptions } from "#/types/settings";
|
||||
import { useAuth } from "#/context/auth-context";
|
||||
|
||||
@@ -67,9 +68,10 @@ function AccountSettings() {
|
||||
|
||||
if (isSuccess) {
|
||||
return (
|
||||
isCustomModel(resources.models, settings.LLM_MODEL) ||
|
||||
isCustomModel(resources.models, settings.llm_model) ||
|
||||
hasAdvancedSettingsSet({
|
||||
...settings,
|
||||
provider_tokens_set: settings.provider_tokens_set || {},
|
||||
})
|
||||
);
|
||||
}
|
||||
@@ -78,12 +80,12 @@ function AccountSettings() {
|
||||
};
|
||||
|
||||
const hasAppSlug = !!config?.APP_SLUG;
|
||||
const isLLMKeySet = settings?.llm_api_key_set === "**********";
|
||||
const isAnalyticsEnabled = settings?.user_consents_to_analytics;
|
||||
const isGitHubTokenSet =
|
||||
providerTokensSet.includes(ProviderOptions.github) || false;
|
||||
const isGitLabTokenSet =
|
||||
providerTokensSet.includes(ProviderOptions.gitlab) || false;
|
||||
const isLLMKeySet = settings?.LLM_API_KEY_SET;
|
||||
const isAnalyticsEnabled = settings?.USER_CONSENTS_TO_ANALYTICS;
|
||||
const isAdvancedSettingsSet = determineWhetherToToggleAdvancedSettings();
|
||||
|
||||
const modelsAndProviders = organizeModelsAndProviders(
|
||||
@@ -94,7 +96,7 @@ function AccountSettings() {
|
||||
"basic" | "advanced"
|
||||
>(isAdvancedSettingsSet ? "advanced" : "basic");
|
||||
const [confirmationModeIsEnabled, setConfirmationModeIsEnabled] =
|
||||
React.useState(!!settings?.SECURITY_ANALYZER);
|
||||
React.useState(!!settings?.security_analyzer);
|
||||
const [resetSettingsModalIsOpen, setResetSettingsModalIsOpen] =
|
||||
React.useState(false);
|
||||
|
||||
@@ -117,6 +119,10 @@ function AccountSettings() {
|
||||
const remoteRuntimeResourceFactor = REMOTE_RUNTIME_OPTIONS.find(
|
||||
({ label }) => label === rawRemoteRuntimeResourceFactor,
|
||||
)?.key;
|
||||
const remoteRuntimeResourceFactorValue = parseInt(
|
||||
remoteRuntimeResourceFactor || "",
|
||||
10,
|
||||
);
|
||||
|
||||
const userConsentsToAnalytics =
|
||||
formData.get("enable-analytics-switch")?.toString() === "on";
|
||||
@@ -142,7 +148,7 @@ function AccountSettings() {
|
||||
: llmBaseUrl;
|
||||
const finalLlmApiKey = shouldHandleSpecialSaasCase ? undefined : llmApiKey;
|
||||
|
||||
const newSettings = {
|
||||
const newSettings: Partial<ServerUserSettings> = {
|
||||
provider_tokens:
|
||||
githubToken || gitlabToken
|
||||
? {
|
||||
@@ -150,21 +156,20 @@ function AccountSettings() {
|
||||
gitlab: gitlabToken || "",
|
||||
}
|
||||
: undefined,
|
||||
LANGUAGE: languageValue,
|
||||
language: languageValue,
|
||||
user_consents_to_analytics: userConsentsToAnalytics,
|
||||
ENABLE_DEFAULT_CONDENSER: enableMemoryCondenser,
|
||||
ENABLE_SOUND_NOTIFICATIONS: enableSoundNotifications,
|
||||
LLM_MODEL: finalLlmModel,
|
||||
LLM_BASE_URL: finalLlmBaseUrl,
|
||||
llm_api_key: finalLlmApiKey,
|
||||
AGENT: formData.get("agent-input")?.toString(),
|
||||
SECURITY_ANALYZER:
|
||||
enable_default_condenser: enableMemoryCondenser,
|
||||
enable_sound_notifications: enableSoundNotifications,
|
||||
llm_model: finalLlmModel,
|
||||
llm_base_url: finalLlmBaseUrl,
|
||||
llm_api_key_set: finalLlmApiKey,
|
||||
agent: formData.get("agent-input")?.toString(),
|
||||
security_analyzer:
|
||||
formData.get("security-analyzer-input")?.toString() || "",
|
||||
REMOTE_RUNTIME_RESOURCE_FACTOR:
|
||||
remoteRuntimeResourceFactor !== null
|
||||
? Number(remoteRuntimeResourceFactor)
|
||||
: DEFAULT_SETTINGS.REMOTE_RUNTIME_RESOURCE_FACTOR,
|
||||
CONFIRMATION_MODE: confirmationModeIsEnabled,
|
||||
remote_runtime_resource_factor:
|
||||
remoteRuntimeResourceFactorValue ||
|
||||
DEFAULT_SETTINGS.remote_runtime_resource_factor,
|
||||
confirmation_mode: confirmationModeIsEnabled,
|
||||
};
|
||||
|
||||
saveSettings(newSettings, {
|
||||
@@ -205,7 +210,7 @@ function AccountSettings() {
|
||||
setLlmConfigMode(isToggled ? "advanced" : "basic");
|
||||
if (!isToggled) {
|
||||
// reset advanced state
|
||||
setConfirmationModeIsEnabled(!!settings?.SECURITY_ANALYZER);
|
||||
setConfirmationModeIsEnabled(!!settings?.security_analyzer);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -249,7 +254,7 @@ function AccountSettings() {
|
||||
{llmConfigMode === "basic" && !shouldHandleSpecialSaasCase && (
|
||||
<ModelSelector
|
||||
models={modelsAndProviders}
|
||||
currentModel={settings.LLM_MODEL}
|
||||
currentModel={settings.llm_model}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -258,7 +263,7 @@ function AccountSettings() {
|
||||
testId="llm-custom-model-input"
|
||||
name="llm-custom-model-input"
|
||||
label={t(I18nKey.SETTINGS$CUSTOM_MODEL)}
|
||||
defaultValue={settings.LLM_MODEL}
|
||||
defaultValue={settings.llm_model}
|
||||
placeholder="anthropic/claude-3-5-sonnet-20241022"
|
||||
type="text"
|
||||
className="w-[680px]"
|
||||
@@ -269,7 +274,7 @@ function AccountSettings() {
|
||||
testId="base-url-input"
|
||||
name="base-url-input"
|
||||
label={t(I18nKey.SETTINGS$BASE_URL)}
|
||||
defaultValue={settings.LLM_BASE_URL}
|
||||
defaultValue={settings.llm_base_url}
|
||||
placeholder="https://api.openai.com"
|
||||
type="text"
|
||||
className="w-[680px]"
|
||||
@@ -310,7 +315,7 @@ function AccountSettings() {
|
||||
label: agent,
|
||||
})) || []
|
||||
}
|
||||
defaultSelectedKey={settings.AGENT}
|
||||
defaultSelectedKey={settings.agent}
|
||||
isClearable={false}
|
||||
/>
|
||||
)}
|
||||
@@ -329,7 +334,7 @@ function AccountSettings() {
|
||||
</>
|
||||
}
|
||||
items={REMOTE_RUNTIME_OPTIONS}
|
||||
defaultSelectedKey={settings.REMOTE_RUNTIME_RESOURCE_FACTOR?.toString()}
|
||||
defaultSelectedKey={settings.remote_runtime_resource_factor?.toString()}
|
||||
isDisabled
|
||||
isClearable={false}
|
||||
/>
|
||||
@@ -339,7 +344,7 @@ function AccountSettings() {
|
||||
<SettingsSwitch
|
||||
testId="enable-confirmation-mode-switch"
|
||||
onToggle={setConfirmationModeIsEnabled}
|
||||
defaultIsToggled={!!settings.CONFIRMATION_MODE}
|
||||
defaultIsToggled={!!settings.confirmation_mode}
|
||||
isBeta
|
||||
>
|
||||
{t(I18nKey.SETTINGS$CONFIRMATION_MODE)}
|
||||
@@ -350,7 +355,7 @@ function AccountSettings() {
|
||||
<SettingsSwitch
|
||||
testId="enable-memory-condenser-switch"
|
||||
name="enable-memory-condenser-switch"
|
||||
defaultIsToggled={!!settings.ENABLE_DEFAULT_CONDENSER}
|
||||
defaultIsToggled={!!settings.enable_default_condenser}
|
||||
>
|
||||
{t(I18nKey.SETTINGS$ENABLE_MEMORY_CONDENSATION)}
|
||||
</SettingsSwitch>
|
||||
@@ -368,7 +373,7 @@ function AccountSettings() {
|
||||
label: analyzer,
|
||||
})) || []
|
||||
}
|
||||
defaultSelectedKey={settings.SECURITY_ANALYZER}
|
||||
defaultSelectedKey={settings.security_analyzer}
|
||||
isClearable
|
||||
showOptionalTag
|
||||
/>
|
||||
@@ -501,7 +506,7 @@ function AccountSettings() {
|
||||
key: language.value,
|
||||
label: language.label,
|
||||
}))}
|
||||
defaultSelectedKey={settings.LANGUAGE}
|
||||
defaultSelectedKey={settings.language}
|
||||
isClearable={false}
|
||||
/>
|
||||
|
||||
@@ -516,7 +521,7 @@ function AccountSettings() {
|
||||
<SettingsSwitch
|
||||
testId="enable-sound-notifications-switch"
|
||||
name="enable-sound-notifications-switch"
|
||||
defaultIsToggled={!!settings.ENABLE_SOUND_NOTIFICATIONS}
|
||||
defaultIsToggled={!!settings.enable_sound_notifications}
|
||||
>
|
||||
{t(I18nKey.SETTINGS$SOUND_NOTIFICATIONS)}
|
||||
</SettingsSwitch>
|
||||
|
||||
@@ -198,13 +198,13 @@ function AppContent() {
|
||||
|
||||
<Controls
|
||||
setSecurityOpen={onSecurityModalOpen}
|
||||
showSecurityLock={!!settings?.SECURITY_ANALYZER}
|
||||
showSecurityLock={!!settings?.security_analyzer}
|
||||
/>
|
||||
{settings && (
|
||||
<Security
|
||||
isOpen={securityModalIsOpen}
|
||||
onOpenChange={onSecurityModalOpenChange}
|
||||
securityAnalyzer={settings.SECURITY_ANALYZER}
|
||||
securityAnalyzer={settings.security_analyzer}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -60,8 +60,8 @@ export default function MainApp() {
|
||||
const navigate = useNavigate();
|
||||
const { pathname } = useLocation();
|
||||
const [searchParams] = useSearchParams();
|
||||
const { data: settings, isFetched: settingsIsFetched } = useSettings();
|
||||
const { providersAreSet } = useAuth();
|
||||
const { data: settings } = useSettings();
|
||||
const { error, isFetching } = useBalance();
|
||||
const { migrateUserConsent } = useMigrateUserConsent();
|
||||
const { t } = useTranslation();
|
||||
@@ -81,17 +81,17 @@ export default function MainApp() {
|
||||
const [consentFormIsOpen, setConsentFormIsOpen] = React.useState(false);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (settings?.LANGUAGE) {
|
||||
i18n.changeLanguage(settings.LANGUAGE);
|
||||
if (settingsIsFetched && settings?.language) {
|
||||
i18n.changeLanguage(settings.language);
|
||||
}
|
||||
}, [settings?.LANGUAGE]);
|
||||
}, [settingsIsFetched, settings?.language]);
|
||||
|
||||
React.useEffect(() => {
|
||||
const consentFormModalIsOpen =
|
||||
settings?.USER_CONSENTS_TO_ANALYTICS === null;
|
||||
settingsIsFetched && settings?.user_consents_to_analytics === null;
|
||||
|
||||
setConsentFormIsOpen(consentFormModalIsOpen);
|
||||
}, [settings]);
|
||||
}, [settingsIsFetched, settings?.user_consents_to_analytics]);
|
||||
|
||||
React.useEffect(() => {
|
||||
// Migrate user consent to the server if it was previously stored in localStorage
|
||||
@@ -148,7 +148,7 @@ export default function MainApp() {
|
||||
|
||||
{config.data?.FEATURE_FLAGS.ENABLE_BILLING &&
|
||||
config.data?.APP_MODE === "saas" &&
|
||||
settings?.IS_NEW_USER && <SetupPaymentModal />}
|
||||
settings?.is_new_user && <SetupPaymentModal />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,28 +1,28 @@
|
||||
import { Settings } from "#/types/settings";
|
||||
import { ClientUserSettings } from "#/api/settings-service/settings-service.types";
|
||||
|
||||
export const LATEST_SETTINGS_VERSION = 5;
|
||||
|
||||
export const DEFAULT_SETTINGS: Settings = {
|
||||
LLM_MODEL: "anthropic/claude-3-5-sonnet-20241022",
|
||||
LLM_BASE_URL: "",
|
||||
AGENT: "CodeActAgent",
|
||||
LANGUAGE: "en",
|
||||
LLM_API_KEY_SET: false,
|
||||
CONFIRMATION_MODE: false,
|
||||
SECURITY_ANALYZER: "",
|
||||
REMOTE_RUNTIME_RESOURCE_FACTOR: 1,
|
||||
PROVIDER_TOKENS_SET: { github: false, gitlab: false },
|
||||
ENABLE_DEFAULT_CONDENSER: true,
|
||||
ENABLE_SOUND_NOTIFICATIONS: false,
|
||||
USER_CONSENTS_TO_ANALYTICS: false,
|
||||
PROVIDER_TOKENS: {
|
||||
github: "",
|
||||
gitlab: "",
|
||||
export const DEFAULT_SETTINGS: ClientUserSettings = {
|
||||
llm_model: "anthropic/claude-3-5-sonnet-20241022",
|
||||
llm_base_url: "",
|
||||
agent: "CodeActAgent",
|
||||
language: "en",
|
||||
llm_api_key_set: null,
|
||||
confirmation_mode: false,
|
||||
security_analyzer: "",
|
||||
remote_runtime_resource_factor: 1,
|
||||
github_token_is_set: false,
|
||||
enable_default_condenser: true,
|
||||
enable_sound_notifications: false,
|
||||
user_consents_to_analytics: false,
|
||||
provider_tokens_set: {
|
||||
github: false,
|
||||
gitlab: false,
|
||||
},
|
||||
IS_NEW_USER: true,
|
||||
is_new_user: true,
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the default settings
|
||||
*/
|
||||
export const getDefaultSettings = (): Settings => DEFAULT_SETTINGS;
|
||||
export const getDefaultSettings = () => DEFAULT_SETTINGS;
|
||||
|
||||
Vendored
-38
@@ -1,38 +0,0 @@
|
||||
import { Provider } from "#/types/settings";
|
||||
|
||||
interface GitHubErrorReponse {
|
||||
message: string;
|
||||
documentation_url: string;
|
||||
status: number;
|
||||
}
|
||||
|
||||
interface GitUser {
|
||||
id: number;
|
||||
login: string;
|
||||
avatar_url: string;
|
||||
company: string | null;
|
||||
name: string | null;
|
||||
email: string | null;
|
||||
}
|
||||
|
||||
interface GitRepository {
|
||||
id: number;
|
||||
full_name: string;
|
||||
git_provider: Provider;
|
||||
stargazers_count?: number;
|
||||
link_header?: string;
|
||||
}
|
||||
|
||||
interface GitHubCommit {
|
||||
html_url: string;
|
||||
sha: string;
|
||||
commit: {
|
||||
author: {
|
||||
date: string; // ISO 8601
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
interface GithubAppInstallation {
|
||||
installations: { id: number }[];
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import { GitProvider } from "#/api/settings-service/settings-service.types";
|
||||
|
||||
export interface GitHubErrorReponse {
|
||||
message: string;
|
||||
documentation_url: string;
|
||||
status: number;
|
||||
}
|
||||
|
||||
export interface GitUser {
|
||||
id: number;
|
||||
login: string;
|
||||
avatar_url: string;
|
||||
company: string | null;
|
||||
name: string | null;
|
||||
email: string | null;
|
||||
}
|
||||
|
||||
export interface GitRepository {
|
||||
id: number;
|
||||
full_name: string;
|
||||
git_provider: GitProvider;
|
||||
stargazers_count?: number;
|
||||
link_header?: string;
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
import { UserSettings } from "#/api/settings-service/settings-service.types";
|
||||
import { DEFAULT_SETTINGS } from "#/services/settings";
|
||||
import { Settings } from "#/types/settings";
|
||||
|
||||
export const hasAdvancedSettingsSet = (settings: Partial<Settings>): boolean =>
|
||||
!!settings.LLM_BASE_URL ||
|
||||
settings.AGENT !== DEFAULT_SETTINGS.AGENT ||
|
||||
settings.REMOTE_RUNTIME_RESOURCE_FACTOR !==
|
||||
DEFAULT_SETTINGS.REMOTE_RUNTIME_RESOURCE_FACTOR ||
|
||||
settings.CONFIRMATION_MODE ||
|
||||
!!settings.SECURITY_ANALYZER;
|
||||
export const hasAdvancedSettingsSet = (settings: UserSettings): boolean =>
|
||||
!!settings.llm_base_url ||
|
||||
settings.agent !== DEFAULT_SETTINGS.agent ||
|
||||
settings.remote_runtime_resource_factor !==
|
||||
DEFAULT_SETTINGS.remote_runtime_resource_factor ||
|
||||
settings.confirmation_mode ||
|
||||
!!settings.security_analyzer;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Settings } from "#/types/settings";
|
||||
import { UserSettings } from "#/api/settings-service/settings-service.types";
|
||||
|
||||
const extractBasicFormData = (formData: FormData) => {
|
||||
const provider = formData.get("llm-provider-input")?.toString();
|
||||
@@ -47,9 +47,7 @@ const extractAdvancedFormData = (formData: FormData) => {
|
||||
};
|
||||
};
|
||||
|
||||
export const extractSettings = (
|
||||
formData: FormData,
|
||||
): Partial<Settings> & { llm_api_key?: string | null } => {
|
||||
export const extractSettings = (formData: FormData): Partial<UserSettings> => {
|
||||
const { LLM_MODEL, LLM_API_KEY, AGENT, LANGUAGE } =
|
||||
extractBasicFormData(formData);
|
||||
|
||||
@@ -74,15 +72,14 @@ export const extractSettings = (
|
||||
}
|
||||
|
||||
return {
|
||||
LLM_MODEL: CUSTOM_LLM_MODEL || LLM_MODEL,
|
||||
LLM_API_KEY_SET: !!LLM_API_KEY,
|
||||
AGENT,
|
||||
LANGUAGE,
|
||||
LLM_BASE_URL,
|
||||
CONFIRMATION_MODE,
|
||||
SECURITY_ANALYZER,
|
||||
ENABLE_DEFAULT_CONDENSER,
|
||||
PROVIDER_TOKENS: providerTokens,
|
||||
llm_api_key: LLM_API_KEY,
|
||||
llm_model: CUSTOM_LLM_MODEL || LLM_MODEL,
|
||||
llm_api_key_set: LLM_API_KEY,
|
||||
agent: AGENT,
|
||||
language: LANGUAGE,
|
||||
llm_base_url: LLM_BASE_URL,
|
||||
confirmation_mode: CONFIRMATION_MODE,
|
||||
security_analyzer: SECURITY_ANALYZER,
|
||||
enable_default_condenser: ENABLE_DEFAULT_CONDENSER,
|
||||
provider_tokens_set: providerTokens,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -82,7 +82,7 @@ async def unset_settings_tokens(request: Request) -> JSONResponse:
|
||||
)
|
||||
|
||||
|
||||
@app.post('/reset-settings', response_model=dict[str, str])
|
||||
@app.post('/settings/reset', response_model=dict[str, str])
|
||||
async def reset_settings(request: Request) -> JSONResponse:
|
||||
"""
|
||||
Resets user settings.
|
||||
|
||||
Reference in New Issue
Block a user