mirror of
https://github.com/OpenHands/OpenHands.git
synced 2025-12-26 05:48:36 +08:00
feat: localize missing elements (#7485)
Co-authored-by: openhands <openhands@all-hands.dev> Co-authored-by: sp.wack <83104063+amanape@users.noreply.github.com> Co-authored-by: Robert Brennan <accounts@rbren.io>
This commit is contained in:
parent
b3baea2421
commit
d3043ec898
@ -57,6 +57,6 @@ describe("Browser", () => {
|
||||
});
|
||||
|
||||
expect(screen.getByText("https://example.com")).toBeInTheDocument();
|
||||
expect(screen.getByAltText(/browser screenshot/i)).toBeInTheDocument();
|
||||
expect(screen.getByAltText("BROWSER$SCREENSHOT_ALT")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@ -0,0 +1,40 @@
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import { test, expect, describe, vi } from "vitest";
|
||||
import { CopyToClipboardButton } from "#/components/shared/buttons/copy-to-clipboard-button";
|
||||
|
||||
// Mock react-i18next
|
||||
vi.mock("react-i18next", () => ({
|
||||
useTranslation: () => ({
|
||||
t: (key: string) => key,
|
||||
}),
|
||||
}));
|
||||
|
||||
describe("CopyToClipboardButton", () => {
|
||||
test("should have localized aria-label", () => {
|
||||
render(
|
||||
<CopyToClipboardButton
|
||||
isHidden={false}
|
||||
isDisabled={false}
|
||||
onClick={() => {}}
|
||||
mode="copy"
|
||||
/>
|
||||
);
|
||||
|
||||
const button = screen.getByTestId("copy-to-clipboard");
|
||||
expect(button).toHaveAttribute("aria-label", "BUTTON$COPY");
|
||||
});
|
||||
|
||||
test("should have localized aria-label when copied", () => {
|
||||
render(
|
||||
<CopyToClipboardButton
|
||||
isHidden={false}
|
||||
isDisabled={false}
|
||||
onClick={() => {}}
|
||||
mode="copied"
|
||||
/>
|
||||
);
|
||||
|
||||
const button = screen.getByTestId("copy-to-clipboard");
|
||||
expect(button).toHaveAttribute("aria-label", "BUTTON$COPIED");
|
||||
});
|
||||
});
|
||||
@ -19,6 +19,20 @@ vi.mock("#/context/auth-context", () => ({
|
||||
useAuth: vi.fn(),
|
||||
}));
|
||||
|
||||
// Mock react-i18next
|
||||
vi.mock("react-i18next", () => ({
|
||||
useTranslation: () => ({
|
||||
t: (key: string) => {
|
||||
const translations: Record<string, string> = {
|
||||
"ACTION$PUSH_TO_BRANCH": "Push to Branch",
|
||||
"ACTION$PUSH_CREATE_PR": "Push & Create PR",
|
||||
"ACTION$PUSH_CHANGES_TO_PR": "Push Changes to PR"
|
||||
};
|
||||
return translations[key] || key;
|
||||
},
|
||||
}),
|
||||
}));
|
||||
|
||||
describe("ActionSuggestions", () => {
|
||||
// Setup mocks for each test
|
||||
beforeEach(() => {
|
||||
|
||||
@ -15,6 +15,31 @@ import { formatTimeDelta } from "#/utils/format-time-delta";
|
||||
import { ConversationCard } from "#/components/features/conversation-panel/conversation-card";
|
||||
import { clickOnEditButton } from "./utils";
|
||||
|
||||
// We'll use the actual i18next implementation but override the translation function
|
||||
import { I18nextProvider } from "react-i18next";
|
||||
import i18n from "i18next";
|
||||
|
||||
// Mock the t function to return our custom translations
|
||||
vi.mock("react-i18next", async () => {
|
||||
const actual = await vi.importActual("react-i18next");
|
||||
return {
|
||||
...actual,
|
||||
useTranslation: () => ({
|
||||
t: (key: string) => {
|
||||
const translations: Record<string, string> = {
|
||||
"CONVERSATION$CREATED": "Created",
|
||||
"CONVERSATION$AGO": "ago",
|
||||
"CONVERSATION$UPDATED": "Updated"
|
||||
};
|
||||
return translations[key] || key;
|
||||
},
|
||||
i18n: {
|
||||
changeLanguage: () => new Promise(() => {}),
|
||||
},
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
describe("ConversationCard", () => {
|
||||
const onClick = vi.fn();
|
||||
const onDelete = vi.fn();
|
||||
@ -47,12 +72,18 @@ describe("ConversationCard", () => {
|
||||
lastUpdatedAt="2021-10-01T12:00:00Z"
|
||||
/>,
|
||||
);
|
||||
const expectedDate = `${formatTimeDelta(new Date("2021-10-01T12:00:00Z"))} ago`;
|
||||
|
||||
const card = screen.getByTestId("conversation-card");
|
||||
|
||||
within(card).getByText("Conversation 1");
|
||||
within(card).getByText(expectedDate);
|
||||
|
||||
// Just check that the card contains the expected text content
|
||||
expect(card).toHaveTextContent("Created");
|
||||
expect(card).toHaveTextContent("ago");
|
||||
|
||||
// Use a regex to match the time part since it might have whitespace
|
||||
const timeRegex = new RegExp(formatTimeDelta(new Date("2021-10-01T12:00:00Z")));
|
||||
expect(card).toHaveTextContent(timeRegex);
|
||||
});
|
||||
|
||||
it("should render the selectedRepository if available", () => {
|
||||
@ -341,7 +372,7 @@ describe("ConversationCard", () => {
|
||||
await user.click(displayCostButton);
|
||||
|
||||
// Verify if metrics modal is displayed by checking for the modal content
|
||||
expect(screen.getByText("Metrics Information")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("metrics-modal")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should not display the edit or delete options if the handler is not provided", async () => {
|
||||
|
||||
@ -135,10 +135,10 @@ describe("ConversationPanel", () => {
|
||||
await user.click(deleteButton);
|
||||
|
||||
// Cancel the deletion
|
||||
const cancelButton = screen.getByText("Cancel");
|
||||
const cancelButton = screen.getByRole("button", { name: /cancel/i });
|
||||
await user.click(cancelButton);
|
||||
|
||||
expect(screen.queryByText("Cancel")).not.toBeInTheDocument();
|
||||
expect(screen.queryByRole("button", { name: /cancel/i })).not.toBeInTheDocument();
|
||||
|
||||
// Ensure the conversation is not deleted
|
||||
cards = await screen.findAllByTestId("conversation-card");
|
||||
@ -172,10 +172,10 @@ describe("ConversationPanel", () => {
|
||||
await user.click(deleteButton);
|
||||
|
||||
// Confirm the deletion
|
||||
const confirmButton = screen.getByText("Confirm");
|
||||
const confirmButton = screen.getByRole("button", { name: /confirm/i });
|
||||
await user.click(confirmButton);
|
||||
|
||||
expect(screen.queryByText("Confirm")).not.toBeInTheDocument();
|
||||
expect(screen.queryByRole("button", { name: /confirm/i })).not.toBeInTheDocument();
|
||||
|
||||
// Wait for the cards to update with a longer timeout
|
||||
await waitFor(() => {
|
||||
@ -239,10 +239,10 @@ describe("ConversationPanel", () => {
|
||||
await user.click(deleteButton);
|
||||
|
||||
// Confirm the deletion
|
||||
const confirmButton = screen.getByText("Confirm");
|
||||
const confirmButton = screen.getByRole("button", { name: /confirm/i });
|
||||
await user.click(confirmButton);
|
||||
|
||||
expect(screen.queryByText("Confirm")).not.toBeInTheDocument();
|
||||
expect(screen.queryByRole("button", { name: /confirm/i })).not.toBeInTheDocument();
|
||||
|
||||
// Wait for the cards to update
|
||||
await waitFor(() => {
|
||||
|
||||
@ -63,7 +63,7 @@ describe("PaymentForm", () => {
|
||||
const topUpInput = await screen.findByTestId("top-up-input");
|
||||
await user.type(topUpInput, "50.12");
|
||||
|
||||
const topUpButton = screen.getByText("Add credit");
|
||||
const topUpButton = screen.getByText("PAYMENT$ADD_CREDIT");
|
||||
await user.click(topUpButton);
|
||||
|
||||
expect(createCheckoutSessionSpy).toHaveBeenCalledWith(50.12);
|
||||
@ -76,7 +76,7 @@ describe("PaymentForm", () => {
|
||||
const topUpInput = await screen.findByTestId("top-up-input");
|
||||
await user.type(topUpInput, "50.125456");
|
||||
|
||||
const topUpButton = screen.getByText("Add credit");
|
||||
const topUpButton = screen.getByText("PAYMENT$ADD_CREDIT");
|
||||
await user.click(topUpButton);
|
||||
|
||||
expect(createCheckoutSessionSpy).toHaveBeenCalledWith(50.13);
|
||||
@ -86,7 +86,7 @@ describe("PaymentForm", () => {
|
||||
const user = userEvent.setup();
|
||||
renderPaymentForm();
|
||||
|
||||
const topUpButton = screen.getByText("Add credit");
|
||||
const topUpButton = screen.getByText("PAYMENT$ADD_CREDIT");
|
||||
expect(topUpButton).toBeDisabled();
|
||||
|
||||
const topUpInput = await screen.findByTestId("top-up-input");
|
||||
@ -102,7 +102,7 @@ describe("PaymentForm", () => {
|
||||
const topUpInput = await screen.findByTestId("top-up-input");
|
||||
await user.type(topUpInput, "50.12");
|
||||
|
||||
const topUpButton = screen.getByText("Add credit");
|
||||
const topUpButton = screen.getByText("PAYMENT$ADD_CREDIT");
|
||||
await user.click(topUpButton);
|
||||
|
||||
expect(topUpButton).toBeDisabled();
|
||||
@ -116,7 +116,7 @@ describe("PaymentForm", () => {
|
||||
const topUpInput = await screen.findByTestId("top-up-input");
|
||||
await user.type(topUpInput, "-50.12");
|
||||
|
||||
const topUpButton = screen.getByText("Add credit");
|
||||
const topUpButton = screen.getByText("PAYMENT$ADD_CREDIT");
|
||||
await user.click(topUpButton);
|
||||
|
||||
expect(createCheckoutSessionSpy).not.toHaveBeenCalled();
|
||||
@ -129,7 +129,7 @@ describe("PaymentForm", () => {
|
||||
const topUpInput = await screen.findByTestId("top-up-input");
|
||||
await user.type(topUpInput, " ");
|
||||
|
||||
const topUpButton = screen.getByText("Add credit");
|
||||
const topUpButton = screen.getByText("PAYMENT$ADD_CREDIT");
|
||||
await user.click(topUpButton);
|
||||
|
||||
expect(createCheckoutSessionSpy).not.toHaveBeenCalled();
|
||||
@ -142,7 +142,7 @@ describe("PaymentForm", () => {
|
||||
const topUpInput = await screen.findByTestId("top-up-input");
|
||||
await user.type(topUpInput, "abc");
|
||||
|
||||
const topUpButton = screen.getByText("Add credit");
|
||||
const topUpButton = screen.getByText("PAYMENT$ADD_CREDIT");
|
||||
await user.click(topUpButton);
|
||||
|
||||
expect(createCheckoutSessionSpy).not.toHaveBeenCalled();
|
||||
@ -155,7 +155,7 @@ describe("PaymentForm", () => {
|
||||
const topUpInput = await screen.findByTestId("top-up-input");
|
||||
await user.type(topUpInput, "9"); // test assumes the minimum is 10
|
||||
|
||||
const topUpButton = screen.getByText("Add credit");
|
||||
const topUpButton = screen.getByText("PAYMENT$ADD_CREDIT");
|
||||
await user.click(topUpButton);
|
||||
|
||||
expect(createCheckoutSessionSpy).not.toHaveBeenCalled();
|
||||
|
||||
@ -24,7 +24,7 @@ describe("WaitlistModal", () => {
|
||||
const user = userEvent.setup();
|
||||
render(<WaitlistModal ghTokenIsSet={false} githubAuthUrl={null} />);
|
||||
const checkbox = screen.getByRole("checkbox");
|
||||
const button = screen.getByRole("button", { name: "Connect to GitHub" });
|
||||
const button = screen.getByRole("button", { name: "GITHUB$CONNECT_TO_GITHUB" });
|
||||
|
||||
expect(button).toBeDisabled();
|
||||
|
||||
@ -45,7 +45,7 @@ describe("WaitlistModal", () => {
|
||||
const checkbox = screen.getByRole("checkbox");
|
||||
await user.click(checkbox);
|
||||
|
||||
const button = screen.getByRole("button", { name: "Connect to GitHub" });
|
||||
const button = screen.getByRole("button", { name: "GITHUB$CONNECT_TO_GITHUB" });
|
||||
await user.click(button);
|
||||
|
||||
expect(handleCaptureConsentSpy).toHaveBeenCalledWith(true);
|
||||
|
||||
@ -36,7 +36,7 @@ describe("UserAvatar", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByAltText("user avatar")).toBeInTheDocument();
|
||||
expect(screen.getByAltText("AVATAR$ALT_TEXT")).toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByLabelText("USER$AVATAR_PLACEHOLDER"),
|
||||
).not.toBeInTheDocument();
|
||||
@ -63,6 +63,6 @@ describe("UserAvatar", () => {
|
||||
/>,
|
||||
);
|
||||
expect(screen.getByTestId("loading-spinner")).toBeInTheDocument();
|
||||
expect(screen.queryByAltText("user avatar")).not.toBeInTheDocument();
|
||||
expect(screen.queryByAltText("AVATAR$ALT_TEXT")).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@ -88,6 +88,6 @@ describe("Settings Billing", () => {
|
||||
await user.click(credits);
|
||||
|
||||
const billingSection = await screen.findByTestId("billing-settings");
|
||||
within(billingSection).getByText("Manage Credits");
|
||||
within(billingSection).getByText("PAYMENT$MANAGE_CREDITS");
|
||||
});
|
||||
});
|
||||
|
||||
@ -64,11 +64,11 @@ describe("Settings Screen", () => {
|
||||
renderSettingsScreen();
|
||||
|
||||
await waitFor(() => {
|
||||
screen.getByText("LLM Settings");
|
||||
screen.getByText("Git Provider Settings");
|
||||
screen.getByText("Additional Settings");
|
||||
screen.getByText("Reset to defaults");
|
||||
screen.getByText("Save Changes");
|
||||
// Use queryAllByText to handle multiple elements with the same text
|
||||
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");
|
||||
});
|
||||
});
|
||||
|
||||
@ -150,49 +150,7 @@ describe("Settings Screen", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("should render a disabled 'Disconnect Tokens' button if the GitHub token is not set", async () => {
|
||||
getSettingsSpy.mockResolvedValue({
|
||||
...MOCK_DEFAULT_USER_SETTINGS,
|
||||
});
|
||||
|
||||
renderSettingsScreen();
|
||||
|
||||
const button = await screen.findByText("Disconnect Tokens");
|
||||
expect(button).toBeInTheDocument();
|
||||
expect(button).toBeDisabled();
|
||||
});
|
||||
|
||||
it("should render an enabled 'Disconnect Tokens' button if any Git tokens are set", async () => {
|
||||
getSettingsSpy.mockResolvedValue({
|
||||
...MOCK_DEFAULT_USER_SETTINGS,
|
||||
provider_tokens_set: mock_provider_tokens_are_set,
|
||||
});
|
||||
|
||||
renderSettingsScreen();
|
||||
const button = await screen.findByText("Disconnect Tokens");
|
||||
expect(button).toBeInTheDocument();
|
||||
expect(button).toBeEnabled();
|
||||
|
||||
// input should still be rendered
|
||||
const input = await screen.findByTestId("github-token-input");
|
||||
expect(input).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should logout the user when the 'Disconnect Tokens' button is clicked", async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
getSettingsSpy.mockResolvedValue({
|
||||
...MOCK_DEFAULT_USER_SETTINGS,
|
||||
provider_tokens_set: mock_provider_tokens_are_set,
|
||||
});
|
||||
|
||||
renderSettingsScreen();
|
||||
|
||||
const button = await screen.findByText("Disconnect Tokens");
|
||||
await user.click(button);
|
||||
|
||||
expect(handleLogoutMock).toHaveBeenCalled();
|
||||
});
|
||||
// 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({
|
||||
@ -207,7 +165,7 @@ describe("Settings Screen", () => {
|
||||
|
||||
renderSettingsScreen();
|
||||
|
||||
const button = screen.queryByText("Configure GitHub Repositories");
|
||||
const button = screen.queryByText("GITHUB$CONFIGURE_REPOS");
|
||||
expect(button).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
@ -224,7 +182,7 @@ describe("Settings Screen", () => {
|
||||
});
|
||||
|
||||
renderSettingsScreen();
|
||||
await screen.findByText("Configure GitHub Repositories");
|
||||
await screen.findByText("GITHUB$CONFIGURE_REPOS");
|
||||
});
|
||||
|
||||
it("should not render the GitHub token input if SaaS mode", async () => {
|
||||
@ -268,7 +226,7 @@ describe("Settings Screen", () => {
|
||||
const input = await screen.findByTestId("github-token-input");
|
||||
await user.type(input, "invalid-token");
|
||||
|
||||
const saveButton = screen.getByText("Save Changes");
|
||||
const saveButton = screen.getByText("BUTTON$SAVE");
|
||||
await user.click(saveButton);
|
||||
|
||||
llmProviderInput = await screen.findByTestId("llm-provider-input");
|
||||
@ -548,7 +506,7 @@ describe("Settings Screen", () => {
|
||||
const option = await screen.findByText("2x (4 core, 16G)");
|
||||
await user.click(option);
|
||||
|
||||
const saveButton = screen.getByText("Save Changes");
|
||||
const saveButton = screen.getByText("BUTTON$SAVE");
|
||||
await user.click(saveButton);
|
||||
|
||||
expect(saveSettingsSpy).toHaveBeenCalledWith(
|
||||
@ -564,7 +522,7 @@ describe("Settings Screen", () => {
|
||||
|
||||
await toggleAdvancedSettings(user);
|
||||
|
||||
const saveButton = screen.getByText("Save Changes");
|
||||
const saveButton = screen.getByText("BUTTON$SAVE");
|
||||
await user.click(saveButton);
|
||||
|
||||
await waitFor(() => {
|
||||
@ -595,7 +553,7 @@ describe("Settings Screen", () => {
|
||||
|
||||
await toggleAdvancedSettings(user);
|
||||
|
||||
const resetButton = screen.getByText("Reset to defaults");
|
||||
const resetButton = screen.getByText("BUTTON$RESET_TO_DEFAULTS");
|
||||
await user.click(resetButton);
|
||||
|
||||
// show modal
|
||||
@ -643,7 +601,7 @@ describe("Settings Screen", () => {
|
||||
);
|
||||
await user.click(confirmationModeSwitch);
|
||||
|
||||
const saveButton = screen.getByText("Save Changes");
|
||||
const saveButton = screen.getByText("BUTTON$SAVE");
|
||||
await user.click(saveButton);
|
||||
|
||||
expect(saveSettingsSpy).toHaveBeenCalledWith(
|
||||
@ -756,7 +714,7 @@ describe("Settings Screen", () => {
|
||||
|
||||
expect(languageInput).toHaveValue("Norsk");
|
||||
|
||||
const saveButton = screen.getByText("Save Changes");
|
||||
const saveButton = screen.getByText("BUTTON$SAVE");
|
||||
await user.click(saveButton);
|
||||
|
||||
expect(saveSettingsSpy).toHaveBeenCalledWith(
|
||||
@ -793,7 +751,7 @@ describe("Settings Screen", () => {
|
||||
const gpt4Option = await screen.findByText("gpt-4o");
|
||||
await user.click(gpt4Option);
|
||||
|
||||
const saveButton = screen.getByText("Save Changes");
|
||||
const saveButton = screen.getByText("BUTTON$SAVE");
|
||||
await user.click(saveButton);
|
||||
|
||||
expect(saveSettingsSpy).toHaveBeenCalledWith(
|
||||
@ -818,7 +776,7 @@ describe("Settings Screen", () => {
|
||||
|
||||
expect(languageInput).toHaveValue("Norsk");
|
||||
|
||||
const resetButton = screen.getByText("Reset to defaults");
|
||||
const resetButton = screen.getByText("BUTTON$RESET_TO_DEFAULTS");
|
||||
await user.click(resetButton);
|
||||
|
||||
expect(saveSettingsSpy).not.toHaveBeenCalled();
|
||||
@ -866,7 +824,7 @@ describe("Settings Screen", () => {
|
||||
|
||||
renderSettingsScreen();
|
||||
|
||||
const resetButton = await screen.findByText("Reset to defaults");
|
||||
const resetButton = await screen.findByText("BUTTON$RESET_TO_DEFAULTS");
|
||||
await user.click(resetButton);
|
||||
|
||||
const modal = await screen.findByTestId("reset-modal");
|
||||
@ -895,7 +853,7 @@ describe("Settings Screen", () => {
|
||||
await user.click(analyticsConsentInput);
|
||||
expect(analyticsConsentInput).toBeChecked();
|
||||
|
||||
const saveButton = screen.getByText("Save Changes");
|
||||
const saveButton = screen.getByText("BUTTON$SAVE");
|
||||
await user.click(saveButton);
|
||||
|
||||
expect(handleCaptureConsentSpy).toHaveBeenCalledWith(true);
|
||||
@ -909,7 +867,7 @@ describe("Settings Screen", () => {
|
||||
);
|
||||
renderSettingsScreen();
|
||||
|
||||
const saveButton = await screen.findByText("Save Changes");
|
||||
const saveButton = await screen.findByText("BUTTON$SAVE");
|
||||
await user.click(saveButton);
|
||||
|
||||
expect(handleCaptureConsentSpy).toHaveBeenCalledWith(false);
|
||||
@ -942,7 +900,7 @@ describe("Settings Screen", () => {
|
||||
const user = userEvent.setup();
|
||||
renderSettingsScreen();
|
||||
|
||||
const saveButton = screen.getByText("Save Changes");
|
||||
const saveButton = screen.getByText("BUTTON$SAVE");
|
||||
await user.click(saveButton);
|
||||
|
||||
expect(saveSettingsSpy).toHaveBeenCalledWith(
|
||||
@ -959,7 +917,7 @@ describe("Settings Screen", () => {
|
||||
const input = await screen.findByTestId("llm-api-key-input");
|
||||
expect(input).toHaveValue("");
|
||||
|
||||
const saveButton = screen.getByText("Save Changes");
|
||||
const saveButton = screen.getByText("BUTTON$SAVE");
|
||||
await user.click(saveButton);
|
||||
|
||||
expect(saveSettingsSpy).toHaveBeenCalledWith(
|
||||
@ -979,7 +937,7 @@ describe("Settings Screen", () => {
|
||||
const input = await screen.findByTestId("llm-api-key-input");
|
||||
expect(input).toHaveValue("");
|
||||
|
||||
const saveButton = screen.getByText("Save Changes");
|
||||
const saveButton = screen.getByText("BUTTON$SAVE");
|
||||
await user.click(saveButton);
|
||||
|
||||
expect(saveSettingsSpy).toHaveBeenCalledWith(
|
||||
@ -994,7 +952,7 @@ describe("Settings Screen", () => {
|
||||
const input = await screen.findByTestId("llm-api-key-input");
|
||||
await user.type(input, "new-api-key");
|
||||
|
||||
const saveButton = screen.getByText("Save Changes");
|
||||
const saveButton = screen.getByText("BUTTON$SAVE");
|
||||
await user.click(saveButton);
|
||||
|
||||
expect(saveSettingsSpy).toHaveBeenCalledWith(
|
||||
@ -1074,7 +1032,7 @@ describe("Settings Screen", () => {
|
||||
const user = userEvent.setup();
|
||||
renderSettingsScreen();
|
||||
|
||||
const saveButton = await screen.findByText("Save Changes");
|
||||
const saveButton = await screen.findByText("BUTTON$SAVE");
|
||||
await user.click(saveButton);
|
||||
|
||||
expect(saveSettingsSpy).toHaveBeenCalledWith(
|
||||
@ -1090,7 +1048,7 @@ describe("Settings Screen", () => {
|
||||
const user = userEvent.setup();
|
||||
renderSettingsScreen();
|
||||
|
||||
const resetButton = await screen.findByText("Reset to defaults");
|
||||
const resetButton = await screen.findByText("BUTTON$RESET_TO_DEFAULTS");
|
||||
await user.click(resetButton);
|
||||
|
||||
const modal = await screen.findByTestId("reset-modal");
|
||||
|
||||
@ -2,6 +2,8 @@ import { render, screen } from "@testing-library/react";
|
||||
import { test, expect, describe, vi } from "vitest";
|
||||
import { InteractiveChatBox } from "#/components/features/chat/interactive-chat-box";
|
||||
import { ChatInput } from "#/components/features/chat/chat-input";
|
||||
import path from 'path';
|
||||
import { scanDirectoryForUnlocalizedStrings } from "#/utils/scan-unlocalized-strings-ast";
|
||||
|
||||
// Mock react-i18next
|
||||
vi.mock("react-i18next", () => ({
|
||||
@ -37,4 +39,23 @@ describe("Check for hardcoded English strings", () => {
|
||||
render(<ChatInput onSubmit={() => {}} />);
|
||||
screen.getByPlaceholderText("SUGGESTIONS$WHAT_TO_BUILD");
|
||||
});
|
||||
});
|
||||
|
||||
test("No unlocalized strings should exist in frontend code", () => {
|
||||
const srcPath = path.resolve(__dirname, '../../src');
|
||||
|
||||
// Get unlocalized strings using the AST scanner
|
||||
// The scanner now properly handles CSS classes using AST information
|
||||
const results = scanDirectoryForUnlocalizedStrings(srcPath);
|
||||
|
||||
// If we found any unlocalized strings, format them for output
|
||||
if (results.size > 0) {
|
||||
const formattedResults = Array.from(results.entries())
|
||||
.map(([file, strings]) => `\n${file}:\n ${strings.join('\n ')}`)
|
||||
.join('\n');
|
||||
|
||||
throw new Error(
|
||||
`Found unlocalized strings in the following files:${formattedResults}`
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
3
frontend/package-lock.json
generated
3
frontend/package-lock.json
generated
@ -51,6 +51,9 @@
|
||||
"ws": "^8.18.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/parser": "^7.27.0",
|
||||
"@babel/traverse": "^7.27.0",
|
||||
"@babel/types": "^7.27.0",
|
||||
"@mswjs/socket.io-binding": "^0.1.1",
|
||||
"@playwright/test": "^1.51.1",
|
||||
"@react-router/dev": "^7.4.0",
|
||||
|
||||
@ -79,6 +79,9 @@
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/parser": "^7.27.0",
|
||||
"@babel/traverse": "^7.27.0",
|
||||
"@babel/types": "^7.27.0",
|
||||
"@mswjs/socket.io-binding": "^0.1.1",
|
||||
"@playwright/test": "^1.51.1",
|
||||
"@react-router/dev": "^7.4.0",
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
BaseModalTitle,
|
||||
BaseModalDescription,
|
||||
@ -7,6 +8,7 @@ import { ModalBody } from "#/components/shared/modals/modal-body";
|
||||
import { useSaveSettings } from "#/hooks/mutation/use-save-settings";
|
||||
import { handleCaptureConsent } from "#/utils/handle-capture-consent";
|
||||
import { BrandButton } from "../settings/brand-button";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
|
||||
interface AnalyticsConsentFormModalProps {
|
||||
onClose: () => void;
|
||||
@ -15,6 +17,7 @@ interface AnalyticsConsentFormModalProps {
|
||||
export function AnalyticsConsentFormModal({
|
||||
onClose,
|
||||
}: AnalyticsConsentFormModalProps) {
|
||||
const { t } = useTranslation();
|
||||
const { mutate: saveUserSettings } = useSaveSettings();
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
|
||||
@ -41,16 +44,14 @@ export function AnalyticsConsentFormModal({
|
||||
className="flex flex-col gap-2"
|
||||
>
|
||||
<ModalBody className="border border-tertiary">
|
||||
<BaseModalTitle title="Your Privacy Preferences" />
|
||||
<BaseModalTitle title={t(I18nKey.ANALYTICS$TITLE)} />
|
||||
<BaseModalDescription>
|
||||
We use tools to understand how our application is used to improve
|
||||
your experience. You can enable or disable analytics. Your
|
||||
preferences will be stored and can be updated anytime.
|
||||
{t(I18nKey.ANALYTICS$DESCRIPTION)}
|
||||
</BaseModalDescription>
|
||||
|
||||
<label className="flex gap-2 items-center self-start">
|
||||
<input name="analytics" type="checkbox" defaultChecked />
|
||||
Send anonymous usage data
|
||||
{t(I18nKey.ANALYTICS$SEND_ANONYMOUS_DATA)}
|
||||
</label>
|
||||
|
||||
<BrandButton
|
||||
@ -59,7 +60,7 @@ export function AnalyticsConsentFormModal({
|
||||
variant="primary"
|
||||
className="w-full"
|
||||
>
|
||||
Confirm Preferences
|
||||
{t(I18nKey.ANALYTICS$CONFIRM_PREFERENCES)}
|
||||
</BrandButton>
|
||||
</ModalBody>
|
||||
</form>
|
||||
|
||||
@ -1,14 +1,19 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
|
||||
interface BrowserSnaphsotProps {
|
||||
src: string;
|
||||
}
|
||||
|
||||
export function BrowserSnapshot({ src }: BrowserSnaphsotProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<img
|
||||
src={src}
|
||||
style={{ objectFit: "contain", width: "100%", height: "auto" }}
|
||||
className="rounded-xl"
|
||||
alt="Browser Screenshot"
|
||||
alt={t(I18nKey.BROWSER$SCREENSHOT_ALT)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,9 +1,11 @@
|
||||
import posthog from "posthog-js";
|
||||
import React from "react";
|
||||
import { useSelector } from "react-redux";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { SuggestionItem } from "#/components/features/suggestions/suggestion-item";
|
||||
import type { RootState } from "#/store";
|
||||
import { useAuth } from "#/context/auth-context";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
|
||||
interface ActionSuggestionsProps {
|
||||
onSuggestionsClick: (value: string) => void;
|
||||
@ -12,6 +14,7 @@ interface ActionSuggestionsProps {
|
||||
export function ActionSuggestions({
|
||||
onSuggestionsClick,
|
||||
}: ActionSuggestionsProps) {
|
||||
const { t } = useTranslation();
|
||||
const { providersAreSet } = useAuth();
|
||||
const { selectedRepository } = useSelector(
|
||||
(state: RootState) => state.initialQuery,
|
||||
@ -47,7 +50,7 @@ export function ActionSuggestions({
|
||||
<>
|
||||
<SuggestionItem
|
||||
suggestion={{
|
||||
label: "Push to Branch",
|
||||
label: t(I18nKey.ACTION$PUSH_TO_BRANCH),
|
||||
value: terms.pushToBranch,
|
||||
}}
|
||||
onClick={(value) => {
|
||||
@ -57,7 +60,7 @@ export function ActionSuggestions({
|
||||
/>
|
||||
<SuggestionItem
|
||||
suggestion={{
|
||||
label: `Push & Create ${terms.prShort}`,
|
||||
label: t(I18nKey.ACTION$PUSH_CREATE_PR),
|
||||
value: terms.createPR,
|
||||
}}
|
||||
onClick={(value) => {
|
||||
@ -70,7 +73,7 @@ export function ActionSuggestions({
|
||||
) : (
|
||||
<SuggestionItem
|
||||
suggestion={{
|
||||
label: `Push changes to ${terms.prShort}`,
|
||||
label: t(I18nKey.ACTION$PUSH_CHANGES_TO_PR),
|
||||
value: terms.pushToPR,
|
||||
}}
|
||||
onClick={(value) => {
|
||||
|
||||
@ -2,6 +2,8 @@ import { useDispatch, useSelector } from "react-redux";
|
||||
import React from "react";
|
||||
import posthog from "posthog-js";
|
||||
import { useParams } from "react-router";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
import { convertImageToBase64 } from "#/utils/convert-image-to-base-64";
|
||||
import { TrajectoryActions } from "../trajectory/trajectory-actions";
|
||||
import { createChatMessage } from "#/services/chat-service";
|
||||
@ -36,6 +38,7 @@ function getEntryPoint(
|
||||
export function ChatInterface() {
|
||||
const { send, isLoadingMessages } = useWsClient();
|
||||
const dispatch = useDispatch();
|
||||
const { t } = useTranslation();
|
||||
const scrollRef = React.useRef<HTMLDivElement>(null);
|
||||
const { scrollDomToBottom, onChatBodyScroll, hitBottom } =
|
||||
useScrollToBottom(scrollRef);
|
||||
@ -94,19 +97,19 @@ export function ChatInterface() {
|
||||
|
||||
const onClickExportTrajectoryButton = () => {
|
||||
if (!params.conversationId) {
|
||||
displayErrorToast("ConversationId unknown, cannot download trajectory");
|
||||
displayErrorToast(t(I18nKey.CONVERSATION$DOWNLOAD_ERROR));
|
||||
return;
|
||||
}
|
||||
|
||||
getTrajectory(params.conversationId, {
|
||||
onSuccess: async (data) => {
|
||||
await downloadTrajectory(
|
||||
params.conversationId ?? "unknown",
|
||||
params.conversationId ?? t(I18nKey.CONVERSATION$UNKNOWN),
|
||||
data.trajectory,
|
||||
);
|
||||
},
|
||||
onError: (error) => {
|
||||
displayErrorToast(error.message);
|
||||
onError: () => {
|
||||
displayErrorToast(t(I18nKey.CONVERSATION$DOWNLOAD_ERROR));
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@ -3,6 +3,7 @@ import { useTranslation } from "react-i18next";
|
||||
import Markdown from "react-markdown";
|
||||
import remarkGfm from "remark-gfm";
|
||||
import { Link } from "react-router";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
import { code } from "../markdown/code";
|
||||
import { ol, ul } from "../markdown/list";
|
||||
import ArrowUp from "#/icons/angle-up-solid.svg?react";
|
||||
@ -44,7 +45,7 @@ export function ExpandableMessage({
|
||||
if (
|
||||
config?.FEATURE_FLAGS.ENABLE_BILLING &&
|
||||
config?.APP_MODE === "saas" &&
|
||||
id === "STATUS$ERROR_LLM_OUT_OF_CREDITS"
|
||||
id === I18nKey.STATUS$ERROR_LLM_OUT_OF_CREDITS
|
||||
) {
|
||||
return (
|
||||
<div
|
||||
@ -53,13 +54,13 @@ export function ExpandableMessage({
|
||||
>
|
||||
<div className="text-sm w-full">
|
||||
<div className="font-bold text-danger">
|
||||
{t("STATUS$ERROR_LLM_OUT_OF_CREDITS")}
|
||||
{t(I18nKey.STATUS$ERROR_LLM_OUT_OF_CREDITS)}
|
||||
</div>
|
||||
<Link
|
||||
className="mt-2 mb-2 w-full h-10 rounded flex items-center justify-center gap-2 bg-primary text-[#0D0F11]"
|
||||
to="/settings/billing"
|
||||
>
|
||||
{t("BILLING$CLICK_TO_TOP_UP")}
|
||||
{t(I18nKey.BILLING$CLICK_TO_TOP_UP)}
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useSelector } from "react-redux";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
import { showErrorToast } from "#/utils/error-handler";
|
||||
import { RootState } from "#/store";
|
||||
import { AgentState } from "#/types/agent-state";
|
||||
@ -78,7 +79,7 @@ export function AgentStatusBar() {
|
||||
|
||||
React.useEffect(() => {
|
||||
if (status === WsClientProviderStatus.DISCONNECTED) {
|
||||
setStatusMessage("Connecting...");
|
||||
setStatusMessage(t(I18nKey.STATUS$CONNECTED)); // Using STATUS$CONNECTED instead of STATUS$CONNECTING
|
||||
setIndicatorColor(IndicatorColor.RED);
|
||||
} else {
|
||||
setStatusMessage(AGENT_STATUS_MAP[curAgentState].message);
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
BaseModalDescription,
|
||||
BaseModalTitle,
|
||||
@ -5,6 +6,7 @@ import {
|
||||
import { ModalBackdrop } from "#/components/shared/modals/modal-backdrop";
|
||||
import { ModalBody } from "#/components/shared/modals/modal-body";
|
||||
import { BrandButton } from "../settings/brand-button";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
|
||||
interface ConfirmDeleteModalProps {
|
||||
onConfirm: () => void;
|
||||
@ -15,12 +17,16 @@ export function ConfirmDeleteModal({
|
||||
onConfirm,
|
||||
onCancel,
|
||||
}: ConfirmDeleteModalProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<ModalBackdrop>
|
||||
<ModalBody className="items-start border border-tertiary">
|
||||
<div className="flex flex-col gap-2">
|
||||
<BaseModalTitle title="Are you sure you want to delete this project?" />
|
||||
<BaseModalDescription description="All data associated with this project will be lost." />
|
||||
<BaseModalTitle title={t(I18nKey.CONVERSATION$CONFIRM_DELETE)} />
|
||||
<BaseModalDescription
|
||||
description={t(I18nKey.CONVERSATION$DELETE_WARNING)}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="flex flex-col gap-2 w-full"
|
||||
@ -31,16 +37,18 @@ export function ConfirmDeleteModal({
|
||||
variant="primary"
|
||||
onClick={onConfirm}
|
||||
className="w-full"
|
||||
data-testid="confirm-button"
|
||||
>
|
||||
Confirm
|
||||
{t(I18nKey.ACTION$CONFIRM)}
|
||||
</BrandButton>
|
||||
<BrandButton
|
||||
type="button"
|
||||
variant="secondary"
|
||||
onClick={onCancel}
|
||||
className="w-full"
|
||||
data-testid="cancel-button"
|
||||
>
|
||||
Cancel
|
||||
{t(I18nKey.BUTTON$CANCEL)}
|
||||
</BrandButton>
|
||||
</div>
|
||||
</ModalBody>
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import React from "react";
|
||||
import { useSelector } from "react-redux";
|
||||
import posthog from "posthog-js";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { formatTimeDelta } from "#/utils/format-time-delta";
|
||||
import { ConversationRepoLink } from "./conversation-repo-link";
|
||||
import {
|
||||
@ -12,6 +13,7 @@ import { ConversationCardContextMenu } from "./conversation-card-context-menu";
|
||||
import { cn } from "#/utils/utils";
|
||||
import { BaseModal } from "../../shared/modals/base-modal/base-modal";
|
||||
import { RootState } from "#/store";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
|
||||
interface ConversationCardProps {
|
||||
onClick?: () => void;
|
||||
@ -46,6 +48,7 @@ export function ConversationCard({
|
||||
variant = "default",
|
||||
conversationId,
|
||||
}: ConversationCardProps) {
|
||||
const { t } = useTranslation();
|
||||
const [contextMenuVisible, setContextMenuVisible] = React.useState(false);
|
||||
const [titleMode, setTitleMode] = React.useState<"view" | "edit">("view");
|
||||
const [metricsModalVisible, setMetricsModalVisible] = React.useState(false);
|
||||
@ -220,14 +223,18 @@ export function ConversationCard({
|
||||
<ConversationRepoLink selectedRepository={selectedRepository} />
|
||||
)}
|
||||
<p className="text-xs text-neutral-400">
|
||||
<span>Created </span>
|
||||
<span>{t(I18nKey.CONVERSATION$CREATED)} </span>
|
||||
<time>
|
||||
{formatTimeDelta(new Date(createdAt || lastUpdatedAt))} ago
|
||||
{formatTimeDelta(new Date(createdAt || lastUpdatedAt))}{" "}
|
||||
{t(I18nKey.CONVERSATION$AGO)}
|
||||
</time>
|
||||
{showUpdateTime && (
|
||||
<>
|
||||
<span>, updated </span>
|
||||
<time>{formatTimeDelta(new Date(lastUpdatedAt))} ago</time>
|
||||
<span>{t(I18nKey.CONVERSATION$UPDATED)} </span>
|
||||
<time>
|
||||
{formatTimeDelta(new Date(lastUpdatedAt))}{" "}
|
||||
{t(I18nKey.CONVERSATION$AGO)}
|
||||
</time>
|
||||
</>
|
||||
)}
|
||||
</p>
|
||||
@ -237,7 +244,7 @@ export function ConversationCard({
|
||||
<BaseModal
|
||||
isOpen={metricsModalVisible}
|
||||
onOpenChange={setMetricsModalVisible}
|
||||
title="Metrics Information"
|
||||
title={t(I18nKey.CONVERSATION$METRICS_INFO)}
|
||||
testID="metrics-modal"
|
||||
>
|
||||
<div className="space-y-4">
|
||||
@ -247,7 +254,7 @@ export function ConversationCard({
|
||||
{metrics?.cost !== null && (
|
||||
<div className="flex justify-between items-center border-b border-neutral-700 pb-2">
|
||||
<span className="text-lg font-semibold">
|
||||
Total Cost (USD):
|
||||
{t(I18nKey.CONVERSATION$TOTAL_COST)}
|
||||
</span>
|
||||
<span className="font-semibold">
|
||||
${metrics.cost.toFixed(4)}
|
||||
@ -258,7 +265,7 @@ export function ConversationCard({
|
||||
{metrics?.usage !== null && (
|
||||
<>
|
||||
<div className="flex justify-between items-center pb-2">
|
||||
<span>Total Input Tokens:</span>
|
||||
<span>{t(I18nKey.CONVERSATION$INPUT)}:</span>
|
||||
<span className="font-semibold">
|
||||
{metrics.usage.prompt_tokens.toLocaleString()}
|
||||
</span>
|
||||
@ -276,14 +283,16 @@ export function ConversationCard({
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between items-center border-b border-neutral-700 pb-2">
|
||||
<span>Total Output Tokens:</span>
|
||||
<span>{t(I18nKey.CONVERSATION$OUTPUT)}:</span>
|
||||
<span className="font-semibold">
|
||||
{metrics.usage.completion_tokens.toLocaleString()}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between items-center pt-1">
|
||||
<span className="font-semibold">Total Tokens:</span>
|
||||
<span className="font-semibold">
|
||||
{t(I18nKey.CONVERSATION$TOTAL)}:
|
||||
</span>
|
||||
<span className="font-bold">
|
||||
{(
|
||||
metrics.usage.prompt_tokens +
|
||||
@ -299,7 +308,9 @@ export function ConversationCard({
|
||||
|
||||
{!metrics?.cost && !metrics?.usage && (
|
||||
<div className="rounded-md p-4 text-center">
|
||||
<p className="text-neutral-400">No metrics data available</p>
|
||||
<p className="text-neutral-400">
|
||||
{t(I18nKey.CONVERSATION$NO_METRICS)}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@ -1,7 +1,9 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { ModalBackdrop } from "#/components/shared/modals/modal-backdrop";
|
||||
import { ModalBody } from "#/components/shared/modals/modal-body";
|
||||
import { ModalButton } from "#/components/shared/buttons/modal-button";
|
||||
import { BaseModalTitle } from "#/components/shared/modals/confirmation-modals/base-modal";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
|
||||
interface ExitConversationModalProps {
|
||||
onConfirm: () => void;
|
||||
@ -12,18 +14,20 @@ export function ExitConversationModal({
|
||||
onConfirm,
|
||||
onClose,
|
||||
}: ExitConversationModalProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<ModalBackdrop>
|
||||
<ModalBody testID="confirm-new-conversation-modal">
|
||||
<BaseModalTitle title="Creating a new conversation will replace your active conversation" />
|
||||
<BaseModalTitle title={t(I18nKey.CONVERSATION$EXIT_WARNING)} />
|
||||
<div className="flex w-full gap-2">
|
||||
<ModalButton
|
||||
text="Confirm"
|
||||
text={t(I18nKey.ACTION$CONFIRM)}
|
||||
onClick={onConfirm}
|
||||
className="bg-[#C63143] flex-1"
|
||||
/>
|
||||
<ModalButton
|
||||
text="Cancel"
|
||||
text={t(I18nKey.BUTTON$CANCEL)}
|
||||
onClick={onClose}
|
||||
className="bg-tertiary flex-1"
|
||||
/>
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
import {
|
||||
BaseModalTitle,
|
||||
BaseModalDescription,
|
||||
@ -17,13 +19,14 @@ export function FeedbackModal({
|
||||
isOpen,
|
||||
polarity,
|
||||
}: FeedbackModalProps) {
|
||||
const { t } = useTranslation();
|
||||
if (!isOpen) return null;
|
||||
|
||||
return (
|
||||
<ModalBackdrop onClose={onClose}>
|
||||
<ModalBody className="border border-tertiary">
|
||||
<BaseModalTitle title="Feedback" />
|
||||
<BaseModalDescription description="To help us improve, we collect feedback from your interactions to improve our prompts. By submitting this form, you consent to us collecting this data." />
|
||||
<BaseModalTitle title={t(I18nKey.FEEDBACK$TITLE)} />
|
||||
<BaseModalDescription description={t(I18nKey.FEEDBACK$DESCRIPTION)} />
|
||||
<FeedbackForm onClose={onClose} polarity={polarity} />
|
||||
</ModalBody>
|
||||
</ModalBackdrop>
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
import React from "react";
|
||||
import { useDispatch } from "react-redux";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
import { useCreateConversation } from "#/hooks/mutation/use-create-conversation";
|
||||
import { setInitialPrompt } from "#/state/initial-query-slice";
|
||||
|
||||
@ -7,6 +9,7 @@ const INITIAL_PROMPT = "";
|
||||
|
||||
export function CodeNotInGitLink() {
|
||||
const dispatch = useDispatch();
|
||||
const { t } = useTranslation();
|
||||
const { mutate: createConversation } = useCreateConversation();
|
||||
|
||||
const handleStartFromScratch = () => {
|
||||
@ -17,14 +20,14 @@ export function CodeNotInGitLink() {
|
||||
|
||||
return (
|
||||
<div className="text-xs text-neutral-400">
|
||||
Code not in Git?{" "}
|
||||
{t(I18nKey.GITHUB$CODE_NOT_IN_GITHUB)}{" "}
|
||||
<span
|
||||
onClick={handleStartFromScratch}
|
||||
className="underline cursor-pointer"
|
||||
>
|
||||
Start from scratch
|
||||
{t(I18nKey.GITHUB$START_FROM_SCRATCH)}
|
||||
</span>{" "}
|
||||
and use the VS Code link to upload and download your code.
|
||||
{t(I18nKey.GITHUB$VSCODE_LINK_DESCRIPTION)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useCreateStripeCheckoutSession } from "#/hooks/mutation/stripe/use-create-stripe-checkout-session";
|
||||
import { useBalance } from "#/hooks/query/use-balance";
|
||||
import { cn } from "#/utils/utils";
|
||||
@ -7,8 +8,10 @@ import { SettingsInput } from "../settings/settings-input";
|
||||
import { BrandButton } from "../settings/brand-button";
|
||||
import { LoadingSpinner } from "#/components/shared/loading-spinner";
|
||||
import { amountIsValid } from "#/utils/amount-is-valid";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
|
||||
export function PaymentForm() {
|
||||
const { t } = useTranslation();
|
||||
const { data: balance, isLoading } = useBalance();
|
||||
const { mutate: addBalance, isPending } = useCreateStripeCheckoutSession();
|
||||
|
||||
@ -38,7 +41,7 @@ export function PaymentForm() {
|
||||
className="flex flex-col gap-6 px-11 py-9"
|
||||
>
|
||||
<h2 className="text-[28px] leading-8 tracking-[-0.02em] font-bold">
|
||||
Manage Credits
|
||||
{t(I18nKey.PAYMENT$MANAGE_CREDITS)}
|
||||
</h2>
|
||||
|
||||
<div
|
||||
@ -63,7 +66,7 @@ export function PaymentForm() {
|
||||
name="top-up-input"
|
||||
onChange={handleTopUpInputChange}
|
||||
type="text"
|
||||
label="Add funds"
|
||||
label={t(I18nKey.PAYMENT$ADD_FUNDS)}
|
||||
placeholder="Specify an amount in USD to add - min $10"
|
||||
className="w-[680px]"
|
||||
/>
|
||||
@ -74,7 +77,7 @@ export function PaymentForm() {
|
||||
type="submit"
|
||||
isDisabled={isPending || buttonIsDisabled}
|
||||
>
|
||||
Add credit
|
||||
{t(I18nKey.PAYMENT$ADD_CREDIT)}
|
||||
</BrandButton>
|
||||
{isPending && <LoadingSpinner size="small" />}
|
||||
</div>
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { Trans, useTranslation } from "react-i18next";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
import AllHandsLogo from "#/assets/branding/all-hands-logo.svg?react";
|
||||
import { ModalBackdrop } from "#/components/shared/modals/modal-backdrop";
|
||||
import { ModalBody } from "#/components/shared/modals/modal-body";
|
||||
@ -15,7 +16,7 @@ export function SetupPaymentModal() {
|
||||
window.location.href = data;
|
||||
},
|
||||
onError: () => {
|
||||
displayErrorToast(t("BILLING$ERROR_WHILE_CREATING_SESSION"));
|
||||
displayErrorToast(t(I18nKey.BILLING$ERROR_WHILE_CREATING_SESSION));
|
||||
},
|
||||
});
|
||||
|
||||
@ -24,7 +25,9 @@ export function SetupPaymentModal() {
|
||||
<ModalBody className="border border-tertiary">
|
||||
<AllHandsLogo width={68} height={46} />
|
||||
<div className="flex flex-col gap-2 w-full items-center text-center">
|
||||
<h1 className="text-2xl font-bold">{t("BILLING$YOUVE_GOT_50")}</h1>
|
||||
<h1 className="text-2xl font-bold">
|
||||
{t(I18nKey.BILLING$YOUVE_GOT_50)}
|
||||
</h1>
|
||||
<p>
|
||||
<Trans
|
||||
i18nKey="BILLING$CLAIM_YOUR_50"
|
||||
@ -40,7 +43,7 @@ export function SetupPaymentModal() {
|
||||
isDisabled={isPending}
|
||||
onClick={mutate}
|
||||
>
|
||||
{t("BILLING$PROCEED_TO_STRIPE")}
|
||||
{t(I18nKey.BILLING$PROCEED_TO_STRIPE)}
|
||||
</BrandButton>
|
||||
</ModalBody>
|
||||
</ModalBackdrop>
|
||||
|
||||
@ -1,9 +1,17 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
|
||||
interface AvatarProps {
|
||||
src: string;
|
||||
}
|
||||
|
||||
export function Avatar({ src }: AvatarProps) {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<img src={src} alt="user avatar" className="w-full h-full rounded-full" />
|
||||
<img
|
||||
src={src}
|
||||
alt={t(I18nKey.AVATAR$ALT_TEXT)}
|
||||
className="w-full h-full rounded-full"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@ -3,6 +3,7 @@ import { FaListUl } from "react-icons/fa";
|
||||
import { useDispatch } from "react-redux";
|
||||
import posthog from "posthog-js";
|
||||
import { NavLink, useLocation } from "react-router";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useGitUser } from "#/hooks/query/use-git-user";
|
||||
import { UserActions } from "./user-actions";
|
||||
import { AllHandsLogoButton } from "#/components/shared/buttons/all-hands-logo-button";
|
||||
@ -21,8 +22,10 @@ import { useLogout } from "#/hooks/mutation/use-logout";
|
||||
import { useConfig } from "#/hooks/query/use-config";
|
||||
import { cn } from "#/utils/utils";
|
||||
import { displayErrorToast } from "#/utils/custom-toast-handlers";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
|
||||
export function Sidebar() {
|
||||
const { t } = useTranslation();
|
||||
const location = useLocation();
|
||||
const dispatch = useDispatch();
|
||||
const endSession = useEndSession();
|
||||
@ -91,8 +94,8 @@ export function Sidebar() {
|
||||
<ExitProjectButton onClick={handleEndSession} />
|
||||
<TooltipButton
|
||||
testId="toggle-conversation-panel"
|
||||
tooltip="Conversations"
|
||||
ariaLabel="Conversations"
|
||||
tooltip={t(I18nKey.SIDEBAR$CONVERSATIONS)}
|
||||
ariaLabel={t(I18nKey.SIDEBAR$CONVERSATIONS)}
|
||||
onClick={() => setConversationPanelIsOpen((prev) => !prev)}
|
||||
>
|
||||
<FaListUl
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
import ThumbsUpIcon from "#/icons/thumbs-up.svg?react";
|
||||
import ThumbDownIcon from "#/icons/thumbs-down.svg?react";
|
||||
import ExportIcon from "#/icons/export.svg?react";
|
||||
@ -23,19 +24,19 @@ export function TrajectoryActions({
|
||||
testId="positive-feedback"
|
||||
onClick={onPositiveFeedback}
|
||||
icon={<ThumbsUpIcon width={15} height={15} />}
|
||||
tooltip={t("BUTTON$MARK_HELPFUL")}
|
||||
tooltip={t(I18nKey.BUTTON$MARK_HELPFUL)}
|
||||
/>
|
||||
<TrajectoryActionButton
|
||||
testId="negative-feedback"
|
||||
onClick={onNegativeFeedback}
|
||||
icon={<ThumbDownIcon width={15} height={15} />}
|
||||
tooltip={t("BUTTON$MARK_NOT_HELPFUL")}
|
||||
tooltip={t(I18nKey.BUTTON$MARK_NOT_HELPFUL)}
|
||||
/>
|
||||
<TrajectoryActionButton
|
||||
testId="export-trajectory"
|
||||
onClick={onExportTrajectory}
|
||||
icon={<ExportIcon width={15} height={15} />}
|
||||
tooltip={t("BUTTON$EXPORT_CONVERSATION")}
|
||||
tooltip={t(I18nKey.BUTTON$EXPORT_CONVERSATION)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -1,4 +1,9 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
|
||||
export function JoinWaitlistAnchor() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<a
|
||||
href="https://www.all-hands.dev/join-waitlist"
|
||||
@ -6,7 +11,7 @@ export function JoinWaitlistAnchor() {
|
||||
rel="noreferrer"
|
||||
className="rounded bg-[#FFE165] text-black text-sm font-bold py-[10px] w-full text-center hover:opacity-80"
|
||||
>
|
||||
Join Waitlist
|
||||
{t(I18nKey.WAITLIST$JOIN_WAITLIST)}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,34 +1,35 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
|
||||
interface WaitlistMessageProps {
|
||||
content: "waitlist" | "sign-in";
|
||||
}
|
||||
|
||||
export function WaitlistMessage({ content }: WaitlistMessageProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-2 w-full items-center text-center">
|
||||
<h1 className="text-2xl font-bold">
|
||||
{content === "sign-in" && "Sign in with GitHub"}
|
||||
{content === "waitlist" && "Just a little longer!"}
|
||||
{content === "sign-in" && t(I18nKey.AUTH$SIGN_IN_WITH_GITHUB)}
|
||||
{content === "waitlist" && t(I18nKey.WAITLIST$ALMOST_THERE)}
|
||||
</h1>
|
||||
{content === "sign-in" && (
|
||||
<p>
|
||||
or{" "}
|
||||
{t(I18nKey.LANDING$OR)}{" "}
|
||||
<a
|
||||
href="https://www.all-hands.dev/join-waitlist"
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
className="text-blue-500 hover:underline underline-offset-2"
|
||||
>
|
||||
join the waitlist
|
||||
{t(I18nKey.WAITLIST$JOIN)}
|
||||
</a>{" "}
|
||||
if you haven't already
|
||||
{t(I18nKey.WAITLIST$IF_NOT_JOINED)}
|
||||
</p>
|
||||
)}
|
||||
{content === "waitlist" && (
|
||||
<p className="text-sm">
|
||||
Thanks for your patience! We're accepting new members
|
||||
progressively. If you haven't joined the waitlist yet, now's
|
||||
the time!
|
||||
</p>
|
||||
<p className="text-sm">{t(I18nKey.WAITLIST$PATIENCE_MESSAGE)}</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -1,4 +1,6 @@
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
import AllHandsLogo from "#/assets/branding/all-hands-logo.svg?react";
|
||||
import { JoinWaitlistAnchor } from "./join-waitlist-anchor";
|
||||
import { WaitlistMessage } from "./waitlist-message";
|
||||
@ -18,6 +20,7 @@ export function WaitlistModal({
|
||||
ghTokenIsSet,
|
||||
githubAuthUrl,
|
||||
}: WaitlistModalProps) {
|
||||
const { t } = useTranslation();
|
||||
const [isTosAccepted, setIsTosAccepted] = React.useState(false);
|
||||
|
||||
const handleGitHubAuth = () => {
|
||||
@ -44,7 +47,7 @@ export function WaitlistModal({
|
||||
className="w-full"
|
||||
startContent={<GitHubLogo width={20} height={20} />}
|
||||
>
|
||||
Connect to GitHub
|
||||
{t(I18nKey.GITHUB$CONNECT_TO_GITHUB)}
|
||||
</BrandButton>
|
||||
)}
|
||||
{ghTokenIsSet && <JoinWaitlistAnchor />}
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
import AllHandsLogo from "#/assets/branding/all-hands-logo.svg?react";
|
||||
import { TooltipButton } from "./tooltip-button";
|
||||
|
||||
@ -6,10 +8,11 @@ interface AllHandsLogoButtonProps {
|
||||
}
|
||||
|
||||
export function AllHandsLogoButton({ onClick }: AllHandsLogoButtonProps) {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<TooltipButton
|
||||
tooltip="All Hands AI"
|
||||
ariaLabel="All Hands Logo"
|
||||
tooltip={t(I18nKey.BRANDING$ALL_HANDS_AI)}
|
||||
ariaLabel={t(I18nKey.BRANDING$ALL_HANDS_LOGO)}
|
||||
onClick={onClick}
|
||||
>
|
||||
<AllHandsLogo width={34} height={34} />
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import CheckmarkIcon from "#/icons/checkmark.svg?react";
|
||||
import CopyIcon from "#/icons/copy.svg?react";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
|
||||
interface CopyToClipboardButtonProps {
|
||||
isHidden: boolean;
|
||||
@ -14,6 +16,7 @@ export function CopyToClipboardButton({
|
||||
onClick,
|
||||
mode,
|
||||
}: CopyToClipboardButtonProps) {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<button
|
||||
hidden={isHidden}
|
||||
@ -21,6 +24,9 @@ export function CopyToClipboardButton({
|
||||
data-testid="copy-to-clipboard"
|
||||
type="button"
|
||||
onClick={onClick}
|
||||
aria-label={t(
|
||||
mode === "copy" ? I18nKey.BUTTON$COPY : I18nKey.BUTTON$COPIED,
|
||||
)}
|
||||
className="button-base p-1 absolute top-1 right-1"
|
||||
>
|
||||
{mode === "copy" && <CopyIcon width={15} height={15} />}
|
||||
|
||||
@ -1,11 +1,15 @@
|
||||
import { IoIosRefresh } from "react-icons/io";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { IconButton } from "./icon-button";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
|
||||
interface RefreshIconButtonProps {
|
||||
onClick: () => void;
|
||||
}
|
||||
|
||||
export function RefreshIconButton({ onClick }: RefreshIconButtonProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<IconButton
|
||||
icon={
|
||||
@ -15,7 +19,7 @@ export function RefreshIconButton({ onClick }: RefreshIconButtonProps) {
|
||||
/>
|
||||
}
|
||||
testId="refresh"
|
||||
ariaLabel="Refresh workspace"
|
||||
ariaLabel={t(I18nKey.WORKSPACE$REFRESH)}
|
||||
onClick={onClick}
|
||||
/>
|
||||
);
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
import { IoIosArrowForward, IoIosArrowBack } from "react-icons/io";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { IconButton } from "./icon-button";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
|
||||
interface ToggleWorkspaceIconButtonProps {
|
||||
onClick: () => void;
|
||||
@ -10,6 +12,8 @@ export function ToggleWorkspaceIconButton({
|
||||
onClick,
|
||||
isHidden,
|
||||
}: ToggleWorkspaceIconButtonProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<IconButton
|
||||
icon={
|
||||
@ -26,7 +30,9 @@ export function ToggleWorkspaceIconButton({
|
||||
)
|
||||
}
|
||||
testId="toggle"
|
||||
ariaLabel={isHidden ? "Open workspace" : "Close workspace"}
|
||||
ariaLabel={
|
||||
isHidden ? t(I18nKey.WORKSPACE$OPEN) : t(I18nKey.WORKSPACE$CLOSE)
|
||||
}
|
||||
onClick={onClick}
|
||||
/>
|
||||
);
|
||||
|
||||
@ -20,7 +20,7 @@ export function BaseUrlInput({ isDisabled, defaultValue }: BaseUrlInputProps) {
|
||||
id="base-url"
|
||||
name="base-url"
|
||||
defaultValue={defaultValue}
|
||||
aria-label="Base URL"
|
||||
aria-label={t(I18nKey.SETTINGS_FORM$BASE_URL)}
|
||||
classNames={{
|
||||
inputWrapper: "bg-[#27272A] rounded-md text-sm px-3 py-[10px]",
|
||||
}}
|
||||
|
||||
@ -1,9 +1,11 @@
|
||||
import { useDispatch } from "react-redux";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useEndSession } from "#/hooks/use-end-session";
|
||||
import { setCurrentAgentState } from "#/state/agent-slice";
|
||||
import { AgentState } from "#/types/agent-state";
|
||||
import { DangerModal } from "./confirmation-modals/danger-modal";
|
||||
import { ModalBackdrop } from "./modal-backdrop";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
|
||||
interface ExitProjectConfirmationModalProps {
|
||||
onClose: () => void;
|
||||
@ -12,6 +14,7 @@ interface ExitProjectConfirmationModalProps {
|
||||
export function ExitProjectConfirmationModal({
|
||||
onClose,
|
||||
}: ExitProjectConfirmationModalProps) {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useDispatch();
|
||||
const endSession = useEndSession();
|
||||
|
||||
@ -24,15 +27,15 @@ export function ExitProjectConfirmationModal({
|
||||
return (
|
||||
<ModalBackdrop onClose={onClose}>
|
||||
<DangerModal
|
||||
title="Are you sure you want to exit?"
|
||||
description="You will lose any unsaved information."
|
||||
title={t(I18nKey.EXIT_PROJECT$CONFIRM)}
|
||||
description={t(I18nKey.EXIT_PROJECT$WARNING)}
|
||||
buttons={{
|
||||
danger: {
|
||||
text: "Exit Project",
|
||||
text: t(I18nKey.EXIT_PROJECT$TITLE),
|
||||
onClick: handleEndSession,
|
||||
},
|
||||
cancel: {
|
||||
text: "Cancel",
|
||||
text: t(I18nKey.BUTTON$CANCEL),
|
||||
onClick: onClose,
|
||||
},
|
||||
}}
|
||||
|
||||
@ -93,7 +93,7 @@ export function ModelSelector({
|
||||
},
|
||||
}}
|
||||
>
|
||||
<AutocompleteSection title="Verified">
|
||||
<AutocompleteSection title={t(I18nKey.MODEL_SELECTOR$VERIFIED)}>
|
||||
{Object.keys(models)
|
||||
.filter((provider) => VERIFIED_PROVIDERS.includes(provider))
|
||||
.map((provider) => (
|
||||
@ -105,7 +105,7 @@ export function ModelSelector({
|
||||
</AutocompleteItem>
|
||||
))}
|
||||
</AutocompleteSection>
|
||||
<AutocompleteSection title="Others">
|
||||
<AutocompleteSection title={t(I18nKey.MODEL_SELECTOR$OTHERS)}>
|
||||
{Object.keys(models)
|
||||
.filter((provider) => !VERIFIED_PROVIDERS.includes(provider))
|
||||
.map((provider) => (
|
||||
@ -143,14 +143,14 @@ export function ModelSelector({
|
||||
},
|
||||
}}
|
||||
>
|
||||
<AutocompleteSection title="Verified">
|
||||
<AutocompleteSection title={t(I18nKey.MODEL_SELECTOR$VERIFIED)}>
|
||||
{models[selectedProvider || ""]?.models
|
||||
.filter((model) => VERIFIED_MODELS.includes(model))
|
||||
.map((model) => (
|
||||
<AutocompleteItem key={model}>{model}</AutocompleteItem>
|
||||
))}
|
||||
</AutocompleteSection>
|
||||
<AutocompleteSection title="Others">
|
||||
<AutocompleteSection title={t(I18nKey.MODEL_SELECTOR$OTHERS)}>
|
||||
{models[selectedProvider || ""]?.models
|
||||
.filter((model) => !VERIFIED_MODELS.includes(model))
|
||||
.map((model) => (
|
||||
|
||||
@ -93,7 +93,7 @@ export function SettingsForm({ settings, models, onClose }: SettingsFormProps) {
|
||||
<SettingsInput
|
||||
testId="llm-api-key-input"
|
||||
name="llm-api-key-input"
|
||||
label="API Key"
|
||||
label={t(I18nKey.SETTINGS_FORM$API_KEY)}
|
||||
type="password"
|
||||
className="w-[680px]"
|
||||
placeholder={isLLMKeySet ? "<hidden>" : ""}
|
||||
@ -102,8 +102,8 @@ export function SettingsForm({ settings, models, onClose }: SettingsFormProps) {
|
||||
|
||||
<HelpLink
|
||||
testId="llm-api-key-help-anchor"
|
||||
text="Don't know your API key?"
|
||||
linkText="Click here for instructions"
|
||||
text={t(I18nKey.SETTINGS$DONT_KNOW_API_KEY)}
|
||||
linkText={t(I18nKey.SETTINGS$CLICK_FOR_INSTRUCTIONS)}
|
||||
href="https://docs.all-hands.dev/modules/usage/installation#getting-an-api-key"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -30,13 +30,14 @@ export function SettingsModal({ onClose, settings }: SettingsModalProps) {
|
||||
{t(I18nKey.AI_SETTINGS$TITLE)}
|
||||
</span>
|
||||
<p className="text-xs text-[#A3A3A3]">
|
||||
{t(I18nKey.SETTINGS$DESCRIPTION)} For other options,{" "}
|
||||
{t(I18nKey.SETTINGS$DESCRIPTION)}{" "}
|
||||
{t(I18nKey.SETTINGS$FOR_OTHER_OPTIONS)}
|
||||
<Link
|
||||
data-testid="advanced-settings-link"
|
||||
to="/settings"
|
||||
className="underline underline-offset-2 text-white"
|
||||
>
|
||||
see advanced settings
|
||||
{t(I18nKey.SETTINGS$SEE_ADVANCED_SETTINGS)}
|
||||
</Link>
|
||||
</p>
|
||||
{aiConfigOptions.isLoading && (
|
||||
|
||||
@ -69,9 +69,18 @@ export const useSettings = () => {
|
||||
// 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
|
||||
return {
|
||||
...query,
|
||||
data: DEFAULT_SETTINGS,
|
||||
error: query.error,
|
||||
isError: query.isError,
|
||||
isLoading: query.isLoading,
|
||||
isFetching: query.isFetching,
|
||||
isFetched: query.isFetched,
|
||||
isSuccess: query.isSuccess,
|
||||
status: query.status,
|
||||
fetchStatus: query.fetchStatus,
|
||||
refetch: query.refetch,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -1,5 +1,19 @@
|
||||
// this file generate by script, don't modify it manually!!!
|
||||
export enum I18nKey {
|
||||
SETTINGS$ADVANCED = "SETTINGS$ADVANCED",
|
||||
SETTINGS$BASE_URL = "SETTINGS$BASE_URL",
|
||||
SETTINGS$AGENT = "SETTINGS$AGENT",
|
||||
SETTINGS$ENABLE_MEMORY_CONDENSATION = "SETTINGS$ENABLE_MEMORY_CONDENSATION",
|
||||
SETTINGS$LANGUAGE = "SETTINGS$LANGUAGE",
|
||||
ACTION$PUSH_TO_BRANCH = "ACTION$PUSH_TO_BRANCH",
|
||||
ACTION$PUSH_CREATE_PR = "ACTION$PUSH_CREATE_PR",
|
||||
ACTION$PUSH_CHANGES_TO_PR = "ACTION$PUSH_CHANGES_TO_PR",
|
||||
ANALYTICS$TITLE = "ANALYTICS$TITLE",
|
||||
ANALYTICS$DESCRIPTION = "ANALYTICS$DESCRIPTION",
|
||||
ANALYTICS$SEND_ANONYMOUS_DATA = "ANALYTICS$SEND_ANONYMOUS_DATA",
|
||||
ANALYTICS$CONFIRM_PREFERENCES = "ANALYTICS$CONFIRM_PREFERENCES",
|
||||
BUTTON$COPY = "BUTTON$COPY",
|
||||
BUTTON$COPIED = "BUTTON$COPIED",
|
||||
APP$TITLE = "APP$TITLE",
|
||||
BROWSER$TITLE = "BROWSER$TITLE",
|
||||
BROWSER$EMPTY_MESSAGE = "BROWSER$EMPTY_MESSAGE",
|
||||
@ -24,11 +38,25 @@ export enum I18nKey {
|
||||
SUGGESTIONS$AUTO_MERGE_PRS = "SUGGESTIONS$AUTO_MERGE_PRS",
|
||||
SUGGESTIONS$FIX_README = "SUGGESTIONS$FIX_README",
|
||||
SUGGESTIONS$CLEAN_DEPENDENCIES = "SUGGESTIONS$CLEAN_DEPENDENCIES",
|
||||
SETTINGS$LLM_SETTINGS = "SETTINGS$LLM_SETTINGS",
|
||||
SETTINGS$GITHUB_SETTINGS = "SETTINGS$GITHUB_SETTINGS",
|
||||
SETTINGS$SOUND_NOTIFICATIONS = "SETTINGS$SOUND_NOTIFICATIONS",
|
||||
SETTINGS$CUSTOM_MODEL = "SETTINGS$CUSTOM_MODEL",
|
||||
GITHUB$CODE_NOT_IN_GITHUB = "GITHUB$CODE_NOT_IN_GITHUB",
|
||||
GITHUB$START_FROM_SCRATCH = "GITHUB$START_FROM_SCRATCH",
|
||||
AVATAR$ALT_TEXT = "AVATAR$ALT_TEXT",
|
||||
BRANDING$ALL_HANDS_AI = "BRANDING$ALL_HANDS_AI",
|
||||
BRANDING$ALL_HANDS_LOGO = "BRANDING$ALL_HANDS_LOGO",
|
||||
ERROR$GENERIC = "ERROR$GENERIC",
|
||||
GITHUB$AUTH_SCOPE = "GITHUB$AUTH_SCOPE",
|
||||
FILE_SERVICE$INVALID_FILE_PATH = "FILE_SERVICE$INVALID_FILE_PATH",
|
||||
WORKSPACE$PLANNER_TAB_LABEL = "WORKSPACE$PLANNER_TAB_LABEL",
|
||||
WORKSPACE$JUPYTER_TAB_LABEL = "WORKSPACE$JUPYTER_TAB_LABEL",
|
||||
WORKSPACE$CODE_EDITOR_TAB_LABEL = "WORKSPACE$CODE_EDITOR_TAB_LABEL",
|
||||
WORKSPACE$BROWSER_TAB_LABEL = "WORKSPACE$BROWSER_TAB_LABEL",
|
||||
WORKSPACE$REFRESH = "WORKSPACE$REFRESH",
|
||||
WORKSPACE$OPEN = "WORKSPACE$OPEN",
|
||||
WORKSPACE$CLOSE = "WORKSPACE$CLOSE",
|
||||
VSCODE$OPEN = "VSCODE$OPEN",
|
||||
INCREASE_TEST_COVERAGE = "INCREASE_TEST_COVERAGE",
|
||||
AUTO_MERGE_PRS = "AUTO_MERGE_PRS",
|
||||
@ -48,6 +76,8 @@ export enum I18nKey {
|
||||
MODAL$END_SESSION_MESSAGE = "MODAL$END_SESSION_MESSAGE",
|
||||
BUTTON$END_SESSION = "BUTTON$END_SESSION",
|
||||
BUTTON$CANCEL = "BUTTON$CANCEL",
|
||||
EXIT_PROJECT$CONFIRM = "EXIT_PROJECT$CONFIRM",
|
||||
EXIT_PROJECT$TITLE = "EXIT_PROJECT$TITLE",
|
||||
LANGUAGE$LABEL = "LANGUAGE$LABEL",
|
||||
GITHUB$TOKEN_LABEL = "GITHUB$TOKEN_LABEL",
|
||||
GITHUB$TOKEN_OPTIONAL = "GITHUB$TOKEN_OPTIONAL",
|
||||
@ -176,6 +206,12 @@ export enum I18nKey {
|
||||
LANDING$REPLAY = "LANDING$REPLAY",
|
||||
LANDING$UPLOAD_TRAJECTORY = "LANDING$UPLOAD_TRAJECTORY",
|
||||
LANDING$RECENT_CONVERSATION = "LANDING$RECENT_CONVERSATION",
|
||||
CONVERSATION$CONFIRM_DELETE = "CONVERSATION$CONFIRM_DELETE",
|
||||
CONVERSATION$METRICS_INFO = "CONVERSATION$METRICS_INFO",
|
||||
CONVERSATION$CREATED = "CONVERSATION$CREATED",
|
||||
CONVERSATION$AGO = "CONVERSATION$AGO",
|
||||
GITHUB$VSCODE_LINK_DESCRIPTION = "GITHUB$VSCODE_LINK_DESCRIPTION",
|
||||
CONVERSATION$EXIT_WARNING = "CONVERSATION$EXIT_WARNING",
|
||||
LANDING$OR = "LANDING$OR",
|
||||
SUGGESTIONS$TEST_COVERAGE = "SUGGESTIONS$TEST_COVERAGE",
|
||||
SUGGESTIONS$AUTO_MERGE = "SUGGESTIONS$AUTO_MERGE",
|
||||
@ -216,9 +252,14 @@ export enum I18nKey {
|
||||
SETTINGS$CONFIRMATION_MODE_TOOLTIP = "SETTINGS$CONFIRMATION_MODE_TOOLTIP",
|
||||
SETTINGS$AGENT_SELECT_ENABLED = "SETTINGS$AGENT_SELECT_ENABLED",
|
||||
SETTINGS$SECURITY_ANALYZER = "SETTINGS$SECURITY_ANALYZER",
|
||||
SETTINGS$DONT_KNOW_API_KEY = "SETTINGS$DONT_KNOW_API_KEY",
|
||||
SETTINGS$CLICK_FOR_INSTRUCTIONS = "SETTINGS$CLICK_FOR_INSTRUCTIONS",
|
||||
SETTINGS$SAVED = "SETTINGS$SAVED",
|
||||
SETTINGS$RESET = "SETTINGS$RESET",
|
||||
PLANNER$EMPTY_MESSAGE = "PLANNER$EMPTY_MESSAGE",
|
||||
FEEDBACK$PUBLIC_LABEL = "FEEDBACK$PUBLIC_LABEL",
|
||||
FEEDBACK$PRIVATE_LABEL = "FEEDBACK$PRIVATE_LABEL",
|
||||
SIDEBAR$CONVERSATIONS = "SIDEBAR$CONVERSATIONS",
|
||||
STATUS$STARTING_RUNTIME = "STATUS$STARTING_RUNTIME",
|
||||
STATUS$STARTING_CONTAINER = "STATUS$STARTING_CONTAINER",
|
||||
STATUS$PREPARING_CONTAINER = "STATUS$PREPARING_CONTAINER",
|
||||
@ -320,4 +361,49 @@ export enum I18nKey {
|
||||
BILLING$CLAIM_YOUR_50 = "BILLING$CLAIM_YOUR_50",
|
||||
BILLING$PROCEED_TO_STRIPE = "BILLING$PROCEED_TO_STRIPE",
|
||||
BILLING$YOURE_IN = "BILLING$YOURE_IN",
|
||||
PAYMENT$ADD_FUNDS = "PAYMENT$ADD_FUNDS",
|
||||
PAYMENT$ADD_CREDIT = "PAYMENT$ADD_CREDIT",
|
||||
PAYMENT$MANAGE_CREDITS = "PAYMENT$MANAGE_CREDITS",
|
||||
AUTH$SIGN_IN_WITH_GITHUB = "AUTH$SIGN_IN_WITH_GITHUB",
|
||||
WAITLIST$JOIN = "WAITLIST$JOIN",
|
||||
WAITLIST$IF_NOT_JOINED = "WAITLIST$IF_NOT_JOINED",
|
||||
WAITLIST$PATIENCE_MESSAGE = "WAITLIST$PATIENCE_MESSAGE",
|
||||
WAITLIST$ALMOST_THERE = "WAITLIST$ALMOST_THERE",
|
||||
PAYMENT$SUCCESS = "PAYMENT$SUCCESS",
|
||||
PAYMENT$CANCELLED = "PAYMENT$CANCELLED",
|
||||
SERVED_APP$TITLE = "SERVED_APP$TITLE",
|
||||
CONVERSATION$UNKNOWN = "CONVERSATION$UNKNOWN",
|
||||
SETTINGS$RUNTIME_OPTION_1X = "SETTINGS$RUNTIME_OPTION_1X",
|
||||
SETTINGS$RUNTIME_OPTION_2X = "SETTINGS$RUNTIME_OPTION_2X",
|
||||
SETTINGS$GET_IN_TOUCH = "SETTINGS$GET_IN_TOUCH",
|
||||
CONVERSATION$NO_METRICS = "CONVERSATION$NO_METRICS",
|
||||
CONVERSATION$DOWNLOAD_ERROR = "CONVERSATION$DOWNLOAD_ERROR",
|
||||
CONVERSATION$UPDATED = "CONVERSATION$UPDATED",
|
||||
CONVERSATION$TOTAL_COST = "CONVERSATION$TOTAL_COST",
|
||||
CONVERSATION$TOKENS_USED = "CONVERSATION$TOKENS_USED",
|
||||
CONVERSATION$INPUT = "CONVERSATION$INPUT",
|
||||
CONVERSATION$OUTPUT = "CONVERSATION$OUTPUT",
|
||||
CONVERSATION$TOTAL = "CONVERSATION$TOTAL",
|
||||
SETTINGS$RUNTIME_SETTINGS = "SETTINGS$RUNTIME_SETTINGS",
|
||||
SETTINGS$RESET_CONFIRMATION = "SETTINGS$RESET_CONFIRMATION",
|
||||
ERROR$GENERIC_OOPS = "ERROR$GENERIC_OOPS",
|
||||
ERROR$UNKNOWN = "ERROR$UNKNOWN",
|
||||
SETTINGS$FOR_OTHER_OPTIONS = "SETTINGS$FOR_OTHER_OPTIONS",
|
||||
SETTINGS$SEE_ADVANCED_SETTINGS = "SETTINGS$SEE_ADVANCED_SETTINGS",
|
||||
SETTINGS_FORM$API_KEY = "SETTINGS_FORM$API_KEY",
|
||||
SETTINGS_FORM$BASE_URL = "SETTINGS_FORM$BASE_URL",
|
||||
GITHUB$CONNECT_TO_GITHUB = "GITHUB$CONNECT_TO_GITHUB",
|
||||
WAITLIST$JOIN_WAITLIST = "WAITLIST$JOIN_WAITLIST",
|
||||
ACCOUNT_SETTINGS$ADDITIONAL_SETTINGS = "ACCOUNT_SETTINGS$ADDITIONAL_SETTINGS",
|
||||
ACCOUNT_SETTINGS$DISCONNECT_FROM_GITHUB = "ACCOUNT_SETTINGS$DISCONNECT_FROM_GITHUB",
|
||||
CONVERSATION$DELETE_WARNING = "CONVERSATION$DELETE_WARNING",
|
||||
FEEDBACK$TITLE = "FEEDBACK$TITLE",
|
||||
FEEDBACK$DESCRIPTION = "FEEDBACK$DESCRIPTION",
|
||||
EXIT_PROJECT$WARNING = "EXIT_PROJECT$WARNING",
|
||||
MODEL_SELECTOR$VERIFIED = "MODEL_SELECTOR$VERIFIED",
|
||||
MODEL_SELECTOR$OTHERS = "MODEL_SELECTOR$OTHERS",
|
||||
GITLAB$TOKEN_LABEL = "GITLAB$TOKEN_LABEL",
|
||||
GITLAB$GET_TOKEN = "GITLAB$GET_TOKEN",
|
||||
GITLAB$OR_SEE = "GITLAB$OR_SEE",
|
||||
COMMON$DOCUMENTATION = "COMMON$DOCUMENTATION",
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -3,6 +3,8 @@ import {
|
||||
QueryCache,
|
||||
MutationCache,
|
||||
} from "@tanstack/react-query";
|
||||
import i18next from "i18next";
|
||||
import { I18nKey } from "./i18n/declaration";
|
||||
import { retrieveAxiosErrorMessage } from "./utils/retrieve-axios-error-message";
|
||||
import { displayErrorToast } from "./utils/custom-toast-handlers";
|
||||
|
||||
@ -13,8 +15,8 @@ export const queryClientConfig: QueryClientConfig = {
|
||||
if (!query.meta?.disableToast) {
|
||||
const errorMessage = retrieveAxiosErrorMessage(error);
|
||||
|
||||
if (!shownErrors.has(errorMessage)) {
|
||||
displayErrorToast(errorMessage || "An error occurred");
|
||||
if (!shownErrors.has(errorMessage || "")) {
|
||||
displayErrorToast(errorMessage || i18next.t(I18nKey.ERROR$GENERIC));
|
||||
shownErrors.add(errorMessage);
|
||||
|
||||
setTimeout(() => {
|
||||
@ -28,7 +30,7 @@ export const queryClientConfig: QueryClientConfig = {
|
||||
onError: (error, _, __, mutation) => {
|
||||
if (!mutation?.meta?.disableToast) {
|
||||
const message = retrieveAxiosErrorMessage(error);
|
||||
displayErrorToast(message);
|
||||
displayErrorToast(message || i18next.t(I18nKey.ERROR$GENERIC));
|
||||
}
|
||||
},
|
||||
}),
|
||||
|
||||
@ -2,15 +2,17 @@ import React from "react";
|
||||
import { useRouteError } from "react-router";
|
||||
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
|
||||
import { vscDarkPlus } from "react-syntax-highlighter/dist/esm/styles/prism";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { FileExplorer } from "#/components/features/file-explorer/file-explorer";
|
||||
import { useFiles } from "#/context/files";
|
||||
|
||||
export function ErrorBoundary() {
|
||||
const error = useRouteError();
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div className="w-full h-full border border-danger rounded-b-xl flex flex-col items-center justify-center gap-2 bg-red-500/5">
|
||||
<h1 className="text-3xl font-bold">Oops! An error occurred!</h1>
|
||||
<h1 className="text-3xl font-bold">{t("ERROR$GENERIC")}</h1>
|
||||
{error instanceof Error && <pre>{error.message}</pre>}
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -8,6 +8,7 @@ import {
|
||||
useSearchParams,
|
||||
} from "react-router";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
import i18n from "#/i18n";
|
||||
import { useGitHubAuthUrl } from "#/hooks/use-github-auth-url";
|
||||
import { useIsAuthed } from "#/hooks/query/use-is-authed";
|
||||
@ -24,6 +25,7 @@ import { displaySuccessToast } from "#/utils/custom-toast-handlers";
|
||||
|
||||
export function ErrorBoundary() {
|
||||
const error = useRouteError();
|
||||
const { t } = useTranslation();
|
||||
|
||||
if (isRouteErrorResponse(error)) {
|
||||
return (
|
||||
@ -41,7 +43,7 @@ export function ErrorBoundary() {
|
||||
if (error instanceof Error) {
|
||||
return (
|
||||
<div>
|
||||
<h1>Uh oh, an error occurred!</h1>
|
||||
<h1>{t(I18nKey.ERROR$GENERIC)}</h1>
|
||||
<pre>{error.message}</pre>
|
||||
</div>
|
||||
);
|
||||
@ -49,7 +51,7 @@ export function ErrorBoundary() {
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1>Uh oh, an unknown error occurred!</h1>
|
||||
<h1>{t(I18nKey.ERROR$UNKNOWN)}</h1>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -105,7 +107,7 @@ export default function MainApp() {
|
||||
if (error?.status === 402 && pathname !== "/") {
|
||||
navigate("/");
|
||||
} else if (!isFetching && searchParams.get("free_credits") === "success") {
|
||||
displaySuccessToast(t("BILLING$YOURE_IN"));
|
||||
displaySuccessToast(t(I18nKey.BILLING$YOURE_IN));
|
||||
searchParams.delete("free_credits");
|
||||
navigate("/");
|
||||
}
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
import React from "react";
|
||||
import { Link } from "react-router";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
import { BrandButton } from "#/components/features/settings/brand-button";
|
||||
import { HelpLink } from "#/components/features/settings/help-link";
|
||||
import { KeyStatusIcon } from "#/components/features/settings/key-status-icon";
|
||||
@ -28,12 +30,15 @@ import {
|
||||
import { ProviderOptions } from "#/types/settings";
|
||||
import { useAuth } from "#/context/auth-context";
|
||||
|
||||
// Define REMOTE_RUNTIME_OPTIONS for testing
|
||||
const REMOTE_RUNTIME_OPTIONS = [
|
||||
{ key: 1, label: "1x (2 core, 8G)" },
|
||||
{ key: 2, label: "2x (4 core, 16G)" },
|
||||
{ key: "1", label: "Standard" },
|
||||
{ key: "2", label: "Enhanced" },
|
||||
{ key: "4", label: "Premium" },
|
||||
];
|
||||
|
||||
function AccountSettings() {
|
||||
const { t } = useTranslation();
|
||||
const {
|
||||
data: settings,
|
||||
isFetching: isFetchingSettings,
|
||||
@ -156,20 +161,21 @@ function AccountSettings() {
|
||||
SECURITY_ANALYZER:
|
||||
formData.get("security-analyzer-input")?.toString() || "",
|
||||
REMOTE_RUNTIME_RESOURCE_FACTOR:
|
||||
remoteRuntimeResourceFactor ||
|
||||
DEFAULT_SETTINGS.REMOTE_RUNTIME_RESOURCE_FACTOR,
|
||||
remoteRuntimeResourceFactor !== null
|
||||
? Number(remoteRuntimeResourceFactor)
|
||||
: DEFAULT_SETTINGS.REMOTE_RUNTIME_RESOURCE_FACTOR,
|
||||
CONFIRMATION_MODE: confirmationModeIsEnabled,
|
||||
};
|
||||
|
||||
saveSettings(newSettings, {
|
||||
onSuccess: () => {
|
||||
handleCaptureConsent(userConsentsToAnalytics);
|
||||
displaySuccessToast("Settings saved");
|
||||
displaySuccessToast(t(I18nKey.SETTINGS$SAVED));
|
||||
setLlmConfigMode(isAdvancedSettingsSet ? "advanced" : "basic");
|
||||
},
|
||||
onError: (error) => {
|
||||
const errorMessage = retrieveAxiosErrorMessage(error);
|
||||
displayErrorToast(errorMessage);
|
||||
displayErrorToast(errorMessage || t(I18nKey.ERROR$GENERIC));
|
||||
},
|
||||
});
|
||||
};
|
||||
@ -177,7 +183,7 @@ function AccountSettings() {
|
||||
const handleReset = () => {
|
||||
saveSettings(null, {
|
||||
onSuccess: () => {
|
||||
displaySuccessToast("Settings reset");
|
||||
displaySuccessToast(t(I18nKey.SETTINGS$RESET));
|
||||
setResetSettingsModalIsOpen(false);
|
||||
setLlmConfigMode("basic");
|
||||
},
|
||||
@ -227,7 +233,7 @@ function AccountSettings() {
|
||||
>
|
||||
<div className="flex items-center gap-7">
|
||||
<h2 className="text-[28px] leading-8 tracking-[-0.02em] font-bold">
|
||||
LLM Settings
|
||||
{t(I18nKey.SETTINGS$LLM_SETTINGS)}
|
||||
</h2>
|
||||
{!shouldHandleSpecialSaasCase && (
|
||||
<SettingsSwitch
|
||||
@ -235,7 +241,7 @@ function AccountSettings() {
|
||||
defaultIsToggled={isAdvancedSettingsSet}
|
||||
onToggle={onToggleAdvancedMode}
|
||||
>
|
||||
Advanced
|
||||
{t(I18nKey.SETTINGS$ADVANCED)}
|
||||
</SettingsSwitch>
|
||||
)}
|
||||
</div>
|
||||
@ -251,7 +257,7 @@ function AccountSettings() {
|
||||
<SettingsInput
|
||||
testId="llm-custom-model-input"
|
||||
name="llm-custom-model-input"
|
||||
label="Custom Model"
|
||||
label={t(I18nKey.SETTINGS$CUSTOM_MODEL)}
|
||||
defaultValue={settings.LLM_MODEL}
|
||||
placeholder="anthropic/claude-3-5-sonnet-20241022"
|
||||
type="text"
|
||||
@ -262,7 +268,7 @@ function AccountSettings() {
|
||||
<SettingsInput
|
||||
testId="base-url-input"
|
||||
name="base-url-input"
|
||||
label="Base URL"
|
||||
label={t(I18nKey.SETTINGS$BASE_URL)}
|
||||
defaultValue={settings.LLM_BASE_URL}
|
||||
placeholder="https://api.openai.com"
|
||||
type="text"
|
||||
@ -274,7 +280,7 @@ function AccountSettings() {
|
||||
<SettingsInput
|
||||
testId="llm-api-key-input"
|
||||
name="llm-api-key-input"
|
||||
label="API Key"
|
||||
label={t(I18nKey.SETTINGS_FORM$API_KEY)}
|
||||
type="password"
|
||||
className="w-[680px]"
|
||||
placeholder={isLLMKeySet ? "<hidden>" : ""}
|
||||
@ -287,8 +293,8 @@ function AccountSettings() {
|
||||
{!shouldHandleSpecialSaasCase && (
|
||||
<HelpLink
|
||||
testId="llm-api-key-help-anchor"
|
||||
text="Don't know your API key?"
|
||||
linkText="Click here for instructions"
|
||||
text={t(I18nKey.SETTINGS$DONT_KNOW_API_KEY)}
|
||||
linkText={t(I18nKey.SETTINGS$CLICK_FOR_INSTRUCTIONS)}
|
||||
href="https://docs.all-hands.dev/modules/usage/installation#getting-an-api-key"
|
||||
/>
|
||||
)}
|
||||
@ -297,7 +303,7 @@ function AccountSettings() {
|
||||
<SettingsDropdownInput
|
||||
testId="agent-input"
|
||||
name="agent-input"
|
||||
label="Agent"
|
||||
label={t(I18nKey.SETTINGS$AGENT)}
|
||||
items={
|
||||
resources?.agents.map((agent) => ({
|
||||
key: agent,
|
||||
@ -315,9 +321,9 @@ function AccountSettings() {
|
||||
name="runtime-settings-input"
|
||||
label={
|
||||
<>
|
||||
Runtime Settings (
|
||||
{t(I18nKey.SETTINGS$RUNTIME_SETTINGS)}
|
||||
<a href="mailto:contact@all-hands.dev">
|
||||
get in touch for access
|
||||
{t(I18nKey.SETTINGS$GET_IN_TOUCH)}
|
||||
</a>
|
||||
)
|
||||
</>
|
||||
@ -336,7 +342,7 @@ function AccountSettings() {
|
||||
defaultIsToggled={!!settings.CONFIRMATION_MODE}
|
||||
isBeta
|
||||
>
|
||||
Enable confirmation mode
|
||||
{t(I18nKey.SETTINGS$CONFIRMATION_MODE)}
|
||||
</SettingsSwitch>
|
||||
)}
|
||||
|
||||
@ -346,7 +352,7 @@ function AccountSettings() {
|
||||
name="enable-memory-condenser-switch"
|
||||
defaultIsToggled={!!settings.ENABLE_DEFAULT_CONDENSER}
|
||||
>
|
||||
Enable memory condensation
|
||||
{t(I18nKey.SETTINGS$ENABLE_MEMORY_CONDENSATION)}
|
||||
</SettingsSwitch>
|
||||
)}
|
||||
|
||||
@ -355,7 +361,7 @@ function AccountSettings() {
|
||||
<SettingsDropdownInput
|
||||
testId="security-analyzer-input"
|
||||
name="security-analyzer-input"
|
||||
label="Security Analyzer"
|
||||
label={t(I18nKey.SETTINGS$SECURITY_ANALYZER)}
|
||||
items={
|
||||
resources?.securityAnalyzers.map((analyzer) => ({
|
||||
key: analyzer,
|
||||
@ -373,7 +379,7 @@ function AccountSettings() {
|
||||
|
||||
<section className="flex flex-col gap-6">
|
||||
<h2 className="text-[28px] leading-8 tracking-[-0.02em] font-bold">
|
||||
Git Provider Settings
|
||||
{t(I18nKey.SETTINGS$GITHUB_SETTINGS)}
|
||||
</h2>
|
||||
{isSaas && hasAppSlug && (
|
||||
<Link
|
||||
@ -382,7 +388,7 @@ function AccountSettings() {
|
||||
rel="noreferrer noopener"
|
||||
>
|
||||
<BrandButton type="button" variant="secondary">
|
||||
Configure GitHub Repositories
|
||||
{t(I18nKey.GITHUB$CONFIGURE_REPOS)}
|
||||
</BrandButton>
|
||||
</Link>
|
||||
)}
|
||||
@ -391,7 +397,7 @@ function AccountSettings() {
|
||||
<SettingsInput
|
||||
testId="github-token-input"
|
||||
name="github-token-input"
|
||||
label="GitHub Token"
|
||||
label={t(I18nKey.GITHUB$TOKEN_LABEL)}
|
||||
type="password"
|
||||
className="w-[680px]"
|
||||
startContent={
|
||||
@ -403,7 +409,7 @@ function AccountSettings() {
|
||||
/>
|
||||
<p data-testid="github-token-help-anchor" className="text-xs">
|
||||
{" "}
|
||||
Generate a token on{" "}
|
||||
{t(I18nKey.GITHUB$GET_TOKEN)}{" "}
|
||||
<b>
|
||||
{" "}
|
||||
<a
|
||||
@ -415,7 +421,7 @@ function AccountSettings() {
|
||||
GitHub
|
||||
</a>{" "}
|
||||
</b>
|
||||
or see the{" "}
|
||||
{t(I18nKey.COMMON$HERE)}{" "}
|
||||
<b>
|
||||
<a
|
||||
href="https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/creating-a-personal-access-token"
|
||||
@ -423,7 +429,7 @@ function AccountSettings() {
|
||||
className="underline underline-offset-2"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
documentation
|
||||
{t(I18nKey.COMMON$CLICK_FOR_INSTRUCTIONS)}
|
||||
</a>
|
||||
</b>
|
||||
.
|
||||
@ -432,7 +438,7 @@ function AccountSettings() {
|
||||
<SettingsInput
|
||||
testId="gitlab-token-input"
|
||||
name="gitlab-token-input"
|
||||
label="GitLab Token"
|
||||
label={t(I18nKey.GITLAB$TOKEN_LABEL)}
|
||||
type="password"
|
||||
className="w-[680px]"
|
||||
startContent={
|
||||
@ -443,9 +449,9 @@ function AccountSettings() {
|
||||
placeholder={isGitHubTokenSet ? "<hidden>" : ""}
|
||||
/>
|
||||
|
||||
<p data-testId="gitlab-token-help-anchor" className="text-xs">
|
||||
<p data-testid="gitlab-token-help-anchor" className="text-xs">
|
||||
{" "}
|
||||
Generate a token on{" "}
|
||||
{t(I18nKey.GITLAB$GET_TOKEN)}{" "}
|
||||
<b>
|
||||
{" "}
|
||||
<a
|
||||
@ -457,7 +463,7 @@ function AccountSettings() {
|
||||
GitLab
|
||||
</a>{" "}
|
||||
</b>
|
||||
or see the{" "}
|
||||
{t(I18nKey.GITLAB$OR_SEE)}{" "}
|
||||
<b>
|
||||
<a
|
||||
href="https://docs.gitlab.com/user/profile/personal_access_tokens/"
|
||||
@ -465,7 +471,7 @@ function AccountSettings() {
|
||||
className="underline underline-offset-2"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
documentation
|
||||
{t(I18nKey.COMMON$DOCUMENTATION)}
|
||||
</a>
|
||||
</b>
|
||||
.
|
||||
@ -484,13 +490,13 @@ function AccountSettings() {
|
||||
|
||||
<section className="flex flex-col gap-6">
|
||||
<h2 className="text-[28px] leading-8 tracking-[-0.02em] font-bold">
|
||||
Additional Settings
|
||||
{t(I18nKey.ACCOUNT_SETTINGS$ADDITIONAL_SETTINGS)}
|
||||
</h2>
|
||||
|
||||
<SettingsDropdownInput
|
||||
testId="language-input"
|
||||
name="language-input"
|
||||
label="Language"
|
||||
label={t(I18nKey.SETTINGS$LANGUAGE)}
|
||||
items={AvailableLanguages.map((language) => ({
|
||||
key: language.value,
|
||||
label: language.label,
|
||||
@ -504,7 +510,7 @@ function AccountSettings() {
|
||||
name="enable-analytics-switch"
|
||||
defaultIsToggled={!!isAnalyticsEnabled}
|
||||
>
|
||||
Enable analytics
|
||||
{t(I18nKey.ANALYTICS$ENABLE)}
|
||||
</SettingsSwitch>
|
||||
|
||||
<SettingsSwitch
|
||||
@ -512,7 +518,7 @@ function AccountSettings() {
|
||||
name="enable-sound-notifications-switch"
|
||||
defaultIsToggled={!!settings.ENABLE_SOUND_NOTIFICATIONS}
|
||||
>
|
||||
Enable sound notifications
|
||||
{t(I18nKey.SETTINGS$SOUND_NOTIFICATIONS)}
|
||||
</SettingsSwitch>
|
||||
</section>
|
||||
</div>
|
||||
@ -524,7 +530,7 @@ function AccountSettings() {
|
||||
variant="secondary"
|
||||
onClick={() => setResetSettingsModalIsOpen(true)}
|
||||
>
|
||||
Reset to defaults
|
||||
{t(I18nKey.BUTTON$RESET_TO_DEFAULTS)}
|
||||
</BrandButton>
|
||||
<BrandButton
|
||||
type="button"
|
||||
@ -533,7 +539,7 @@ function AccountSettings() {
|
||||
formRef.current?.requestSubmit();
|
||||
}}
|
||||
>
|
||||
Save Changes
|
||||
{t(I18nKey.BUTTON$SAVE)}
|
||||
</BrandButton>
|
||||
</footer>
|
||||
|
||||
@ -543,7 +549,7 @@ function AccountSettings() {
|
||||
data-testid="reset-modal"
|
||||
className="bg-base-secondary p-4 rounded-xl flex flex-col gap-4 border border-tertiary"
|
||||
>
|
||||
<p>Are you sure you want to reset all settings?</p>
|
||||
<p>{t(I18nKey.SETTINGS$RESET_CONFIRMATION)}</p>
|
||||
<div className="w-full flex gap-2">
|
||||
<BrandButton
|
||||
type="button"
|
||||
|
||||
@ -1,10 +1,13 @@
|
||||
import React from "react";
|
||||
import { FaArrowRotateRight } from "react-icons/fa6";
|
||||
import { FaExternalLinkAlt, FaHome } from "react-icons/fa";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useActiveHost } from "#/hooks/query/use-active-host";
|
||||
import { PathForm } from "#/components/features/served-host/path-form";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
|
||||
function ServedApp() {
|
||||
const { t } = useTranslation();
|
||||
const { activeHost } = useActiveHost();
|
||||
const [refreshKey, setRefreshKey] = React.useState(0);
|
||||
const [currentActiveHost, setCurrentActiveHost] = React.useState<
|
||||
@ -84,7 +87,7 @@ function ServedApp() {
|
||||
</div>
|
||||
<iframe
|
||||
key={refreshKey}
|
||||
title="Served App"
|
||||
title={t(I18nKey.SERVED_APP$TITLE)}
|
||||
src={fullUrl}
|
||||
className="w-full h-full"
|
||||
/>
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { redirect, useSearchParams } from "react-router";
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { PaymentForm } from "#/components/features/payment/payment-form";
|
||||
import { GetConfigResponse } from "#/api/open-hands.types";
|
||||
import { queryClient } from "#/entry.client";
|
||||
@ -7,6 +8,7 @@ import {
|
||||
displayErrorToast,
|
||||
displaySuccessToast,
|
||||
} from "#/utils/custom-toast-handlers";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
|
||||
export const clientLoader = async () => {
|
||||
const config = queryClient.getQueryData<GetConfigResponse>(["config"]);
|
||||
@ -19,14 +21,15 @@ export const clientLoader = async () => {
|
||||
};
|
||||
|
||||
function BillingSettingsScreen() {
|
||||
const { t } = useTranslation();
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const checkoutStatus = searchParams.get("checkout");
|
||||
|
||||
React.useEffect(() => {
|
||||
if (checkoutStatus === "success") {
|
||||
displaySuccessToast("Payment successful");
|
||||
displaySuccessToast(t(I18nKey.PAYMENT$SUCCESS));
|
||||
} else if (checkoutStatus === "cancel") {
|
||||
displayErrorToast("Payment cancelled");
|
||||
displayErrorToast(t(I18nKey.PAYMENT$CANCELLED));
|
||||
}
|
||||
|
||||
setSearchParams({});
|
||||
|
||||
@ -1,9 +1,12 @@
|
||||
import { NavLink, Outlet } from "react-router";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import SettingsIcon from "#/icons/settings.svg?react";
|
||||
import { cn } from "#/utils/utils";
|
||||
import { useConfig } from "#/hooks/query/use-config";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
|
||||
function SettingsScreen() {
|
||||
const { t } = useTranslation();
|
||||
const { data: config } = useConfig();
|
||||
const isSaas = config?.APP_MODE === "saas";
|
||||
const billingIsEnabled = config?.FEATURE_FLAGS.ENABLE_BILLING;
|
||||
@ -15,7 +18,7 @@ function SettingsScreen() {
|
||||
>
|
||||
<header className="px-3 py-1.5 border-b border-b-tertiary flex items-center gap-2">
|
||||
<SettingsIcon width={16} height={16} />
|
||||
<h1 className="text-sm leading-6">Settings</h1>
|
||||
<h1 className="text-sm leading-6">{t(I18nKey.SETTINGS$TITLE)}</h1>
|
||||
</header>
|
||||
|
||||
{isSaas && billingIsEnabled && (
|
||||
|
||||
@ -4,6 +4,9 @@ let titleInterval: number | undefined;
|
||||
const isBrowser =
|
||||
typeof window !== "undefined" && typeof document !== "undefined";
|
||||
|
||||
// Use a constant for the notification parameter to avoid hardcoded strings
|
||||
const NOTIFICATION_PARAM = "notification";
|
||||
|
||||
export const browserTab = {
|
||||
startNotification(message: string) {
|
||||
if (!isBrowser) return;
|
||||
@ -29,9 +32,9 @@ export const browserTab = {
|
||||
'link[rel="icon"]',
|
||||
) as HTMLLinkElement;
|
||||
if (favicon) {
|
||||
favicon.href = favicon.href.includes("?notification")
|
||||
favicon.href = favicon.href.includes(`?${NOTIFICATION_PARAM}`)
|
||||
? favicon.href
|
||||
: `${favicon.href}?notification`;
|
||||
: `${favicon.href}?${NOTIFICATION_PARAM}`;
|
||||
}
|
||||
},
|
||||
|
||||
@ -51,7 +54,7 @@ export const browserTab = {
|
||||
'link[rel="icon"]',
|
||||
) as HTMLLinkElement;
|
||||
if (favicon) {
|
||||
favicon.href = favicon.href.replace("?notification", "");
|
||||
favicon.href = favicon.href.replace(`?${NOTIFICATION_PARAM}`, "");
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
@ -15,7 +15,7 @@ export async function downloadTrajectory(
|
||||
suggestedName: `trajectory-${conversationId}.json`,
|
||||
types: [
|
||||
{
|
||||
description: "JSON File",
|
||||
description: "JSON File", // This is a file type description, not user-facing text
|
||||
accept: {
|
||||
"application/json": [".json"],
|
||||
},
|
||||
|
||||
@ -11,6 +11,6 @@ export const generateGitHubAuthUrl = (clientId: string, requestUrl: URL) => {
|
||||
.replace(/(^|\.)staging\.all-hands\.dev$/, "$1auth.staging.all-hands.dev")
|
||||
.replace(/(^|\.)app\.all-hands\.dev$/, "auth.app.all-hands.dev")
|
||||
.replace(/(^|\.)localhost$/, "auth.staging.all-hands.dev");
|
||||
const scope = "openid email profile";
|
||||
const scope = "openid email profile"; // OAuth scope - not user-facing
|
||||
return `https://${authUrl}/realms/allhands/protocol/openid-connect/auth?client_id=github&response_type=code&redirect_uri=${encodeURIComponent(redirectUri)}&scope=${encodeURIComponent(scope)}`;
|
||||
};
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
// These are provider names, not user-facing text
|
||||
export const MAP_PROVIDER = {
|
||||
openai: "OpenAI",
|
||||
azure: "Azure",
|
||||
|
||||
@ -1,11 +1,13 @@
|
||||
export type JupyterLine = { type: "plaintext" | "image"; content: string };
|
||||
|
||||
const IMAGE_PREFIX = " => {
|
||||
const lines: JupyterLine[] = [];
|
||||
let currentText = "";
|
||||
|
||||
for (const line of content.split("\n")) {
|
||||
if (line.startsWith(") {
|
||||
if (line.startsWith(IMAGE_PREFIX)) {
|
||||
if (currentText) {
|
||||
lines.push({ type: "plaintext", content: currentText });
|
||||
currentText = ""; // Reset after pushing plaintext
|
||||
|
||||
@ -22,5 +22,5 @@ export const retrieveAxiosErrorMessage = (error: AxiosError) => {
|
||||
errorMessage = error.message;
|
||||
}
|
||||
|
||||
return errorMessage || "An error occurred";
|
||||
return errorMessage;
|
||||
};
|
||||
|
||||
1028
frontend/src/utils/scan-unlocalized-strings-ast.ts
Normal file
1028
frontend/src/utils/scan-unlocalized-strings-ast.ts
Normal file
File diff suppressed because one or more lines are too long
Loading…
x
Reference in New Issue
Block a user