mirror of
https://github.com/All-Hands-AI/OpenHands.git
synced 2026-04-29 03:00:45 -04:00
Compare commits
2 Commits
self-hoste
...
chore/brea
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c47503af9a | ||
|
|
d3044d4e00 |
@@ -0,0 +1,25 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
import { SettingsSwitch } from "../settings-switch";
|
||||
|
||||
interface AdvancedSettingsSwitchProps {
|
||||
defaultIsToggled: boolean;
|
||||
onToggle: (isToggled: boolean) => void;
|
||||
}
|
||||
|
||||
export function AdvancedSettingsSwitch({
|
||||
defaultIsToggled,
|
||||
onToggle,
|
||||
}: AdvancedSettingsSwitchProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<SettingsSwitch
|
||||
testId="advanced-settings-switch"
|
||||
defaultIsToggled={defaultIsToggled}
|
||||
onToggle={onToggle}
|
||||
>
|
||||
{t(I18nKey.SETTINGS$ADVANCED)}
|
||||
</SettingsSwitch>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
import { SettingsDropdownInput } from "../settings-dropdown-input";
|
||||
|
||||
interface AgentInputProps {
|
||||
agents: string[];
|
||||
defaultAgent: string;
|
||||
}
|
||||
|
||||
export function AgentInput({ agents, defaultAgent }: AgentInputProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<SettingsDropdownInput
|
||||
testId="agent-input"
|
||||
name="agent-input"
|
||||
label={t(I18nKey.SETTINGS$AGENT)}
|
||||
items={
|
||||
agents.map((agent) => ({
|
||||
key: agent,
|
||||
label: agent,
|
||||
})) || []
|
||||
}
|
||||
defaultSelectedKey={defaultAgent}
|
||||
isClearable={false}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { SettingsInput } from "../settings-input";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
|
||||
interface BaseUrlInputProps {
|
||||
defaultBaseUrl: string;
|
||||
}
|
||||
|
||||
export function BaseUrlInput({ defaultBaseUrl }: BaseUrlInputProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<SettingsInput
|
||||
testId="base-url-input"
|
||||
name="base-url-input"
|
||||
label={t(I18nKey.SETTINGS$BASE_URL)}
|
||||
defaultValue={defaultBaseUrl}
|
||||
placeholder="https://api.openai.com"
|
||||
type="text"
|
||||
className="w-[680px]"
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { ModalBackdrop } from "#/components/shared/modals/modal-backdrop";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
import { BrandButton } from "../brand-button";
|
||||
|
||||
interface ConfirmResetSettingsModalProps {
|
||||
handleReset: () => void;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export function ConfirmResetSettingsModal({
|
||||
handleReset,
|
||||
onClose,
|
||||
}: ConfirmResetSettingsModalProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<ModalBackdrop>
|
||||
<div
|
||||
data-testid="reset-modal"
|
||||
className="bg-base-secondary p-4 rounded-xl flex flex-col gap-4 border border-tertiary"
|
||||
>
|
||||
<p>{t(I18nKey.SETTINGS$RESET_CONFIRMATION)}</p>
|
||||
<div className="w-full flex gap-2">
|
||||
<BrandButton
|
||||
type="button"
|
||||
variant="primary"
|
||||
className="grow"
|
||||
onClick={handleReset}
|
||||
>
|
||||
Reset
|
||||
</BrandButton>
|
||||
|
||||
<BrandButton
|
||||
type="button"
|
||||
variant="secondary"
|
||||
className="grow"
|
||||
onClick={onClose}
|
||||
>
|
||||
Cancel
|
||||
</BrandButton>
|
||||
</div>
|
||||
</div>
|
||||
</ModalBackdrop>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
import { SettingsSwitch } from "../settings-switch";
|
||||
|
||||
interface EnableAnalyticsSwitchProps {
|
||||
defaultIsToggled: boolean;
|
||||
}
|
||||
|
||||
export function EnableAnalyticsSwitch({
|
||||
defaultIsToggled,
|
||||
}: EnableAnalyticsSwitchProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<SettingsSwitch
|
||||
testId="enable-analytics-switch"
|
||||
name="enable-analytics-switch"
|
||||
defaultIsToggled={defaultIsToggled}
|
||||
>
|
||||
{t(I18nKey.ANALYTICS$ENABLE)}
|
||||
</SettingsSwitch>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
import { SettingsSwitch } from "../settings-switch";
|
||||
|
||||
interface EnableConfirmationModeSwitchProps {
|
||||
onToggle: (isEnabled: boolean) => void;
|
||||
defaultIsToggled: boolean;
|
||||
}
|
||||
|
||||
export function EnableConfirmationModeSwitch({
|
||||
defaultIsToggled,
|
||||
onToggle,
|
||||
}: EnableConfirmationModeSwitchProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<SettingsSwitch
|
||||
testId="enable-confirmation-mode-switch"
|
||||
onToggle={onToggle}
|
||||
defaultIsToggled={defaultIsToggled}
|
||||
isBeta
|
||||
>
|
||||
{t(I18nKey.SETTINGS$CONFIRMATION_MODE)}
|
||||
</SettingsSwitch>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
import { SettingsSwitch } from "../settings-switch";
|
||||
|
||||
interface EnableMemoryCondensorSwitchProps {
|
||||
defaultIsToggled: boolean;
|
||||
}
|
||||
|
||||
export function EnableMemoryCondensorSwitch({
|
||||
defaultIsToggled,
|
||||
}: EnableMemoryCondensorSwitchProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<SettingsSwitch
|
||||
testId="enable-memory-condenser-switch"
|
||||
name="enable-memory-condenser-switch"
|
||||
defaultIsToggled={defaultIsToggled}
|
||||
>
|
||||
{t(I18nKey.SETTINGS$ENABLE_MEMORY_CONDENSATION)}
|
||||
</SettingsSwitch>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
import { SettingsSwitch } from "../settings-switch";
|
||||
|
||||
export function EnableSoundNotificationsSwitch({
|
||||
defaultIsToggled,
|
||||
}: {
|
||||
defaultIsToggled: boolean;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<SettingsSwitch
|
||||
testId="enable-sound-notifications-switch"
|
||||
name="enable-sound-notifications-switch"
|
||||
defaultIsToggled={defaultIsToggled}
|
||||
>
|
||||
{t(I18nKey.SETTINGS$SOUND_NOTIFICATIONS)}
|
||||
</SettingsSwitch>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
import { KeyStatusIcon } from "../key-status-icon";
|
||||
import { SettingsInput } from "../settings-input";
|
||||
|
||||
interface GitHubTokenInputProps {
|
||||
isGitHubTokenSet: boolean;
|
||||
}
|
||||
|
||||
export function GitHubTokenInput({ isGitHubTokenSet }: GitHubTokenInputProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<>
|
||||
<SettingsInput
|
||||
testId="github-token-input"
|
||||
name="github-token-input"
|
||||
label={t(I18nKey.GITHUB$TOKEN_LABEL)}
|
||||
type="password"
|
||||
className="w-[680px]"
|
||||
startContent={
|
||||
isGitHubTokenSet && <KeyStatusIcon isSet={!!isGitHubTokenSet} />
|
||||
}
|
||||
placeholder={isGitHubTokenSet ? "<hidden>" : ""}
|
||||
/>
|
||||
<p data-testid="github-token-help-anchor" className="text-xs">
|
||||
{" "}
|
||||
{t(I18nKey.GITHUB$GET_TOKEN)}{" "}
|
||||
<b>
|
||||
{" "}
|
||||
<a
|
||||
href="https://github.com/settings/tokens/new?description=openhands-app&scopes=repo,user,workflow"
|
||||
target="_blank"
|
||||
className="underline underline-offset-2"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
GitHub
|
||||
</a>{" "}
|
||||
</b>
|
||||
{t(I18nKey.COMMON$HERE)}{" "}
|
||||
<b>
|
||||
<a
|
||||
href="https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/creating-a-personal-access-token"
|
||||
target="_blank"
|
||||
className="underline underline-offset-2"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{t(I18nKey.COMMON$CLICK_FOR_INSTRUCTIONS)}
|
||||
</a>
|
||||
</b>
|
||||
.
|
||||
</p>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
interface GitLabTokenInputProps {
|
||||
isGitLabTokenSet: boolean;
|
||||
}
|
||||
|
||||
export function GitLabTokenInput({ isGitLabTokenSet }: GitLabTokenInputProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<>
|
||||
<SettingsInput
|
||||
testId="gitlab-token-input"
|
||||
name="gitlab-token-input"
|
||||
label={t(I18nKey.GITHUB$TOKEN_LABEL)}
|
||||
type="password"
|
||||
className="w-[680px]"
|
||||
startContent={
|
||||
isGitLabTokenSet && <KeyStatusIcon isSet={!!isGitLabTokenSet} />
|
||||
}
|
||||
placeholder={isGitLabTokenSet ? "<hidden>" : ""}
|
||||
/>
|
||||
|
||||
<p data-testid="gitlab-token-help-anchor" className="text-xs">
|
||||
{" "}
|
||||
{t(I18nKey.GITHUB$GET_TOKEN)}{" "}
|
||||
<b>
|
||||
{" "}
|
||||
<a
|
||||
href="https://gitlab.com/-/user_settings/personal_access_tokens?name=openhands-app&scopes=api,read_user,read_repository,write_repository"
|
||||
target="_blank"
|
||||
className="underline underline-offset-2"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
GitLab
|
||||
</a>{" "}
|
||||
</b>
|
||||
{t(I18nKey.GITLAB$OR_SEE)}{" "}
|
||||
<b>
|
||||
<a
|
||||
href="https://docs.gitlab.com/user/profile/personal_access_tokens/"
|
||||
target="_blank"
|
||||
className="underline underline-offset-2"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{t(I18nKey.COMMON$CLICK_FOR_INSTRUCTIONS)}
|
||||
</a>
|
||||
</b>
|
||||
.
|
||||
</p>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { AvailableLanguages } from "#/i18n";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
import { SettingsDropdownInput } from "../settings-dropdown-input";
|
||||
|
||||
interface LanguageInputProps {
|
||||
defaultLanguage: string;
|
||||
}
|
||||
|
||||
export function LanguageInput({ defaultLanguage }: LanguageInputProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<SettingsDropdownInput
|
||||
testId="language-input"
|
||||
name="language-input"
|
||||
label={t(I18nKey.SETTINGS$LANGUAGE)}
|
||||
items={AvailableLanguages.map((language) => ({
|
||||
key: language.value,
|
||||
label: language.label,
|
||||
}))}
|
||||
defaultSelectedKey={defaultLanguage}
|
||||
isClearable={false}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
import { HelpLink } from "../help-link";
|
||||
import { KeyStatusIcon } from "../key-status-icon";
|
||||
import { SettingsInput } from "../settings-input";
|
||||
|
||||
interface LlmApiKeyInputProps {
|
||||
isLLMKeySet: boolean;
|
||||
}
|
||||
|
||||
export function LlmApiKeyInput({ isLLMKeySet }: LlmApiKeyInputProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<>
|
||||
<SettingsInput
|
||||
testId="llm-api-key-input"
|
||||
name="llm-api-key-input"
|
||||
label={t(I18nKey.SETTINGS_FORM$API_KEY)}
|
||||
type="password"
|
||||
className="w-[680px]"
|
||||
placeholder={isLLMKeySet ? "<hidden>" : ""}
|
||||
startContent={isLLMKeySet && <KeyStatusIcon isSet={isLLMKeySet} />}
|
||||
/>
|
||||
|
||||
<HelpLink
|
||||
testId="llm-api-key-help-anchor"
|
||||
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"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
import { SettingsInput } from "../settings-input";
|
||||
|
||||
interface LlmCustomModelInputProps {
|
||||
defaultModel: string;
|
||||
}
|
||||
|
||||
export function LlmCustomModelInput({
|
||||
defaultModel,
|
||||
}: LlmCustomModelInputProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<SettingsInput
|
||||
testId="llm-custom-model-input"
|
||||
name="llm-custom-model-input"
|
||||
label={t(I18nKey.SETTINGS$CUSTOM_MODEL)}
|
||||
defaultValue={defaultModel}
|
||||
placeholder="anthropic/claude-3-5-sonnet-20241022"
|
||||
type="text"
|
||||
className="w-[680px]"
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { SettingsDropdownInput } from "../settings-dropdown-input";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
|
||||
// Define REMOTE_RUNTIME_OPTIONS for testing
|
||||
const REMOTE_RUNTIME_OPTIONS = [
|
||||
{ key: "1", label: "Standard" },
|
||||
{ key: "2", label: "Enhanced" },
|
||||
{ key: "4", label: "Premium" },
|
||||
];
|
||||
|
||||
interface RuntimeSettingsInputProps {
|
||||
defaultRuntime?: string;
|
||||
}
|
||||
|
||||
export function RuntimeSettingsInput({
|
||||
defaultRuntime,
|
||||
}: RuntimeSettingsInputProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<SettingsDropdownInput
|
||||
testId="runtime-settings-input"
|
||||
name="runtime-settings-input"
|
||||
label={
|
||||
<>
|
||||
{t(I18nKey.SETTINGS$RUNTIME_SETTINGS)}
|
||||
<a href="mailto:contact@all-hands.dev">
|
||||
{t(I18nKey.SETTINGS$GET_IN_TOUCH)}
|
||||
</a>
|
||||
</>
|
||||
}
|
||||
items={REMOTE_RUNTIME_OPTIONS}
|
||||
defaultSelectedKey={defaultRuntime}
|
||||
isDisabled
|
||||
isClearable={false}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { SettingsDropdownInput } from "../settings-dropdown-input";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
|
||||
interface SecurityAnalzerInputProps {
|
||||
securityAnalyzers: string[];
|
||||
defaultSecurityAnalyzer: string;
|
||||
}
|
||||
|
||||
export function SecurityAnalzerInput({
|
||||
defaultSecurityAnalyzer,
|
||||
securityAnalyzers,
|
||||
}: SecurityAnalzerInputProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<SettingsDropdownInput
|
||||
testId="security-analyzer-input"
|
||||
name="security-analyzer-input"
|
||||
label={t(I18nKey.SETTINGS$SECURITY_ANALYZER)}
|
||||
items={securityAnalyzers.map((analyzer) => ({
|
||||
key: analyzer,
|
||||
label: analyzer,
|
||||
}))}
|
||||
defaultSelectedKey={defaultSecurityAnalyzer}
|
||||
isClearable
|
||||
showOptionalTag
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
import { Input } from "@heroui/react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
|
||||
interface BaseUrlInputProps {
|
||||
isDisabled: boolean;
|
||||
defaultValue: string;
|
||||
}
|
||||
|
||||
export function BaseUrlInput({ isDisabled, defaultValue }: BaseUrlInputProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<fieldset className="flex flex-col gap-2">
|
||||
<label htmlFor="base-url" className="font-[500] text-[#A3A3A3] text-xs">
|
||||
{t(I18nKey.SETTINGS_FORM$BASE_URL_LABEL)}
|
||||
</label>
|
||||
<Input
|
||||
isDisabled={isDisabled}
|
||||
id="base-url"
|
||||
name="base-url"
|
||||
defaultValue={defaultValue}
|
||||
aria-label={t(I18nKey.SETTINGS_FORM$BASE_URL)}
|
||||
classNames={{
|
||||
inputWrapper: "bg-[#27272A] rounded-md text-sm px-3 py-[10px]",
|
||||
}}
|
||||
/>
|
||||
</fieldset>
|
||||
);
|
||||
}
|
||||
@@ -3,21 +3,13 @@ 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";
|
||||
import { SettingsDropdownInput } from "#/components/features/settings/settings-dropdown-input";
|
||||
import { SettingsInput } from "#/components/features/settings/settings-input";
|
||||
import { SettingsSwitch } from "#/components/features/settings/settings-switch";
|
||||
import { LoadingSpinner } from "#/components/shared/loading-spinner";
|
||||
import { ModalBackdrop } from "#/components/shared/modals/modal-backdrop";
|
||||
import { ModelSelector } from "#/components/shared/modals/settings/model-selector";
|
||||
import { useSaveSettings } from "#/hooks/mutation/use-save-settings";
|
||||
import { useAIConfigOptions } from "#/hooks/query/use-ai-config-options";
|
||||
import { useConfig } from "#/hooks/query/use-config";
|
||||
import { useSettings } from "#/hooks/query/use-settings";
|
||||
import { useAppLogout } from "#/hooks/use-app-logout";
|
||||
import { AvailableLanguages } from "#/i18n";
|
||||
import { DEFAULT_SETTINGS } from "#/services/settings";
|
||||
import { handleCaptureConsent } from "#/utils/handle-capture-consent";
|
||||
import { hasAdvancedSettingsSet } from "#/utils/has-advanced-settings-set";
|
||||
import { isCustomModel } from "#/utils/is-custom-model";
|
||||
@@ -29,13 +21,24 @@ import {
|
||||
} from "#/utils/custom-toast-handlers";
|
||||
import { ProviderOptions } from "#/types/settings";
|
||||
import { useAuth } from "#/context/auth-context";
|
||||
|
||||
// Define REMOTE_RUNTIME_OPTIONS for testing
|
||||
const REMOTE_RUNTIME_OPTIONS = [
|
||||
{ key: "1", label: "Standard" },
|
||||
{ key: "2", label: "Enhanced" },
|
||||
{ key: "4", label: "Premium" },
|
||||
];
|
||||
import { LlmCustomModelInput } from "#/components/features/settings/account/llm-custom-model-input";
|
||||
import { BaseUrlInput } from "#/components/features/settings/account/base-url-input";
|
||||
import { AgentInput } from "#/components/features/settings/account/agent-input";
|
||||
import { RuntimeSettingsInput } from "#/components/features/settings/account/runtime-settings-input";
|
||||
import { EnableConfirmationModeSwitch } from "#/components/features/settings/account/enable-confirmation-mode-switch";
|
||||
import { EnableMemoryCondensorSwitch } from "#/components/features/settings/account/enable-memory-condensor-switch";
|
||||
import { SecurityAnalzerInput } from "#/components/features/settings/account/security-analyzer-input";
|
||||
import { EnableAnalyticsSwitch } from "#/components/features/settings/account/enable-analytics-switch";
|
||||
import { EnableSoundNotificationsSwitch } from "#/components/features/settings/account/enable-sound-notification-switch";
|
||||
import { LanguageInput } from "#/components/features/settings/account/language-input";
|
||||
import { buildUserPreferences } from "#/utils/build-user-preferences";
|
||||
import {
|
||||
GitHubTokenInput,
|
||||
GitLabTokenInput,
|
||||
} from "#/components/features/settings/account/git-provider-token-input";
|
||||
import { ConfirmResetSettingsModal } from "#/components/features/settings/account/confirm-reset-settings-modal";
|
||||
import { AdvancedSettingsSwitch } from "#/components/features/settings/account/advanced-settings-switch";
|
||||
import { LlmApiKeyInput } from "#/components/features/settings/account/llm-api-key-input";
|
||||
|
||||
function AccountSettings() {
|
||||
const { t } = useTranslation();
|
||||
@@ -100,76 +103,20 @@ function AccountSettings() {
|
||||
|
||||
const formRef = React.useRef<HTMLFormElement>(null);
|
||||
|
||||
/**
|
||||
* Submits the user's preferences to the server.
|
||||
*/
|
||||
const onSubmit = async (formData: FormData) => {
|
||||
const languageLabel = formData.get("language-input")?.toString();
|
||||
const languageValue = AvailableLanguages.find(
|
||||
({ label }) => label === languageLabel,
|
||||
)?.value;
|
||||
|
||||
const llmProvider = formData.get("llm-provider-input")?.toString();
|
||||
const llmModel = formData.get("llm-model-input")?.toString();
|
||||
const fullLlmModel = `${llmProvider}/${llmModel}`.toLowerCase();
|
||||
const customLlmModel = formData.get("llm-custom-model-input")?.toString();
|
||||
|
||||
const rawRemoteRuntimeResourceFactor = formData
|
||||
.get("runtime-settings-input")
|
||||
?.toString();
|
||||
const remoteRuntimeResourceFactor = REMOTE_RUNTIME_OPTIONS.find(
|
||||
({ label }) => label === rawRemoteRuntimeResourceFactor,
|
||||
)?.key;
|
||||
|
||||
const userConsentsToAnalytics =
|
||||
formData.get("enable-analytics-switch")?.toString() === "on";
|
||||
const enableMemoryCondenser =
|
||||
formData.get("enable-memory-condenser-switch")?.toString() === "on";
|
||||
const enableSoundNotifications =
|
||||
formData.get("enable-sound-notifications-switch")?.toString() === "on";
|
||||
const llmBaseUrl = formData.get("base-url-input")?.toString().trim() || "";
|
||||
const inputApiKey = formData.get("llm-api-key-input")?.toString() || "";
|
||||
const llmApiKey =
|
||||
inputApiKey === "" && isLLMKeySet
|
||||
? undefined // don't update if it's already set and input is empty
|
||||
: inputApiKey; // otherwise use the input value
|
||||
|
||||
const githubToken = formData.get("github-token-input")?.toString();
|
||||
const gitlabToken = formData.get("gitlab-token-input")?.toString();
|
||||
// we don't want the user to be able to modify these settings in SaaS
|
||||
const finalLlmModel = shouldHandleSpecialSaasCase
|
||||
? undefined
|
||||
: customLlmModel || fullLlmModel;
|
||||
const finalLlmBaseUrl = shouldHandleSpecialSaasCase
|
||||
? undefined
|
||||
: llmBaseUrl;
|
||||
const finalLlmApiKey = shouldHandleSpecialSaasCase ? undefined : llmApiKey;
|
||||
|
||||
const newSettings = {
|
||||
provider_tokens:
|
||||
githubToken || gitlabToken
|
||||
? {
|
||||
github: githubToken || "",
|
||||
gitlab: gitlabToken || "",
|
||||
}
|
||||
: undefined,
|
||||
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:
|
||||
formData.get("security-analyzer-input")?.toString() || "",
|
||||
REMOTE_RUNTIME_RESOURCE_FACTOR:
|
||||
remoteRuntimeResourceFactor !== null
|
||||
? Number(remoteRuntimeResourceFactor)
|
||||
: DEFAULT_SETTINGS.REMOTE_RUNTIME_RESOURCE_FACTOR,
|
||||
CONFIRMATION_MODE: confirmationModeIsEnabled,
|
||||
};
|
||||
const newSettings = buildUserPreferences(
|
||||
formData,
|
||||
isLLMKeySet,
|
||||
shouldHandleSpecialSaasCase,
|
||||
confirmationModeIsEnabled,
|
||||
);
|
||||
|
||||
saveSettings(newSettings, {
|
||||
onSuccess: () => {
|
||||
handleCaptureConsent(userConsentsToAnalytics);
|
||||
handleCaptureConsent(newSettings.user_consents_to_analytics);
|
||||
displaySuccessToast(t(I18nKey.SETTINGS$SAVED));
|
||||
setLlmConfigMode(isAdvancedSettingsSet ? "advanced" : "basic");
|
||||
},
|
||||
@@ -180,6 +127,9 @@ function AccountSettings() {
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Resets the user's settings to the default values.
|
||||
*/
|
||||
const handleReset = () => {
|
||||
saveSettings(null, {
|
||||
onSuccess: () => {
|
||||
@@ -236,13 +186,10 @@ function AccountSettings() {
|
||||
{t(I18nKey.SETTINGS$LLM_SETTINGS)}
|
||||
</h2>
|
||||
{!shouldHandleSpecialSaasCase && (
|
||||
<SettingsSwitch
|
||||
testId="advanced-settings-switch"
|
||||
<AdvancedSettingsSwitch
|
||||
defaultIsToggled={isAdvancedSettingsSet}
|
||||
onToggle={onToggleAdvancedMode}
|
||||
>
|
||||
{t(I18nKey.SETTINGS$ADVANCED)}
|
||||
</SettingsSwitch>
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -254,125 +201,48 @@ function AccountSettings() {
|
||||
)}
|
||||
|
||||
{llmConfigMode === "advanced" && !shouldHandleSpecialSaasCase && (
|
||||
<SettingsInput
|
||||
testId="llm-custom-model-input"
|
||||
name="llm-custom-model-input"
|
||||
label={t(I18nKey.SETTINGS$CUSTOM_MODEL)}
|
||||
defaultValue={settings.LLM_MODEL}
|
||||
placeholder="anthropic/claude-3-5-sonnet-20241022"
|
||||
type="text"
|
||||
className="w-[680px]"
|
||||
/>
|
||||
<LlmCustomModelInput defaultModel={settings.LLM_MODEL} />
|
||||
)}
|
||||
|
||||
{llmConfigMode === "advanced" && !shouldHandleSpecialSaasCase && (
|
||||
<SettingsInput
|
||||
testId="base-url-input"
|
||||
name="base-url-input"
|
||||
label={t(I18nKey.SETTINGS$BASE_URL)}
|
||||
defaultValue={settings.LLM_BASE_URL}
|
||||
placeholder="https://api.openai.com"
|
||||
type="text"
|
||||
className="w-[680px]"
|
||||
/>
|
||||
<BaseUrlInput defaultBaseUrl={settings.LLM_BASE_URL} />
|
||||
)}
|
||||
|
||||
{!shouldHandleSpecialSaasCase && (
|
||||
<SettingsInput
|
||||
testId="llm-api-key-input"
|
||||
name="llm-api-key-input"
|
||||
label={t(I18nKey.SETTINGS_FORM$API_KEY)}
|
||||
type="password"
|
||||
className="w-[680px]"
|
||||
placeholder={isLLMKeySet ? "<hidden>" : ""}
|
||||
startContent={
|
||||
isLLMKeySet && <KeyStatusIcon isSet={isLLMKeySet} />
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
||||
{!shouldHandleSpecialSaasCase && (
|
||||
<HelpLink
|
||||
testId="llm-api-key-help-anchor"
|
||||
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"
|
||||
/>
|
||||
<LlmApiKeyInput isLLMKeySet={!!isLLMKeySet} />
|
||||
)}
|
||||
|
||||
{llmConfigMode === "advanced" && (
|
||||
<SettingsDropdownInput
|
||||
testId="agent-input"
|
||||
name="agent-input"
|
||||
label={t(I18nKey.SETTINGS$AGENT)}
|
||||
items={
|
||||
resources?.agents.map((agent) => ({
|
||||
key: agent,
|
||||
label: agent,
|
||||
})) || []
|
||||
}
|
||||
defaultSelectedKey={settings.AGENT}
|
||||
isClearable={false}
|
||||
<AgentInput
|
||||
agents={resources?.agents || []}
|
||||
defaultAgent={settings.AGENT}
|
||||
/>
|
||||
)}
|
||||
|
||||
{isSaas && llmConfigMode === "advanced" && (
|
||||
<SettingsDropdownInput
|
||||
testId="runtime-settings-input"
|
||||
name="runtime-settings-input"
|
||||
label={
|
||||
<>
|
||||
{t(I18nKey.SETTINGS$RUNTIME_SETTINGS)}
|
||||
<a href="mailto:contact@all-hands.dev">
|
||||
{t(I18nKey.SETTINGS$GET_IN_TOUCH)}
|
||||
</a>
|
||||
)
|
||||
</>
|
||||
}
|
||||
items={REMOTE_RUNTIME_OPTIONS}
|
||||
defaultSelectedKey={settings.REMOTE_RUNTIME_RESOURCE_FACTOR?.toString()}
|
||||
isDisabled
|
||||
isClearable={false}
|
||||
<RuntimeSettingsInput
|
||||
defaultRuntime={settings.REMOTE_RUNTIME_RESOURCE_FACTOR?.toString()}
|
||||
/>
|
||||
)}
|
||||
|
||||
{llmConfigMode === "advanced" && (
|
||||
<SettingsSwitch
|
||||
testId="enable-confirmation-mode-switch"
|
||||
<EnableConfirmationModeSwitch
|
||||
onToggle={setConfirmationModeIsEnabled}
|
||||
defaultIsToggled={!!settings.CONFIRMATION_MODE}
|
||||
isBeta
|
||||
>
|
||||
{t(I18nKey.SETTINGS$CONFIRMATION_MODE)}
|
||||
</SettingsSwitch>
|
||||
/>
|
||||
)}
|
||||
|
||||
{llmConfigMode === "advanced" && (
|
||||
<SettingsSwitch
|
||||
testId="enable-memory-condenser-switch"
|
||||
name="enable-memory-condenser-switch"
|
||||
<EnableMemoryCondensorSwitch
|
||||
defaultIsToggled={!!settings.ENABLE_DEFAULT_CONDENSER}
|
||||
>
|
||||
{t(I18nKey.SETTINGS$ENABLE_MEMORY_CONDENSATION)}
|
||||
</SettingsSwitch>
|
||||
/>
|
||||
)}
|
||||
|
||||
{llmConfigMode === "advanced" && confirmationModeIsEnabled && (
|
||||
<div>
|
||||
<SettingsDropdownInput
|
||||
testId="security-analyzer-input"
|
||||
name="security-analyzer-input"
|
||||
label={t(I18nKey.SETTINGS$SECURITY_ANALYZER)}
|
||||
items={
|
||||
resources?.securityAnalyzers.map((analyzer) => ({
|
||||
key: analyzer,
|
||||
label: analyzer,
|
||||
})) || []
|
||||
}
|
||||
defaultSelectedKey={settings.SECURITY_ANALYZER}
|
||||
isClearable
|
||||
showOptionalTag
|
||||
/>
|
||||
</div>
|
||||
<SecurityAnalzerInput
|
||||
securityAnalyzers={resources?.securityAnalyzers || []}
|
||||
defaultSecurityAnalyzer={settings.SECURITY_ANALYZER}
|
||||
/>
|
||||
)}
|
||||
</section>
|
||||
)}
|
||||
@@ -381,6 +251,7 @@ function AccountSettings() {
|
||||
<h2 className="text-[28px] leading-8 tracking-[-0.02em] font-bold">
|
||||
{t(I18nKey.SETTINGS$GITHUB_SETTINGS)}
|
||||
</h2>
|
||||
|
||||
{isSaas && hasAppSlug && (
|
||||
<Link
|
||||
to={`https://github.com/apps/${config.APP_SLUG}/installations/new`}
|
||||
@@ -392,90 +263,12 @@ function AccountSettings() {
|
||||
</BrandButton>
|
||||
</Link>
|
||||
)}
|
||||
|
||||
{!isSaas && (
|
||||
<>
|
||||
<SettingsInput
|
||||
testId="github-token-input"
|
||||
name="github-token-input"
|
||||
label={t(I18nKey.GITHUB$TOKEN_LABEL)}
|
||||
type="password"
|
||||
className="w-[680px]"
|
||||
startContent={
|
||||
isGitHubTokenSet && (
|
||||
<KeyStatusIcon isSet={!!isGitHubTokenSet} />
|
||||
)
|
||||
}
|
||||
placeholder={isGitHubTokenSet ? "<hidden>" : ""}
|
||||
/>
|
||||
<p data-testid="github-token-help-anchor" className="text-xs">
|
||||
{" "}
|
||||
{t(I18nKey.GITHUB$GET_TOKEN)}{" "}
|
||||
<b>
|
||||
{" "}
|
||||
<a
|
||||
href="https://github.com/settings/tokens/new?description=openhands-app&scopes=repo,user,workflow"
|
||||
target="_blank"
|
||||
className="underline underline-offset-2"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
GitHub
|
||||
</a>{" "}
|
||||
</b>
|
||||
{t(I18nKey.COMMON$HERE)}{" "}
|
||||
<b>
|
||||
<a
|
||||
href="https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/creating-a-personal-access-token"
|
||||
target="_blank"
|
||||
className="underline underline-offset-2"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{t(I18nKey.COMMON$CLICK_FOR_INSTRUCTIONS)}
|
||||
</a>
|
||||
</b>
|
||||
.
|
||||
</p>
|
||||
<GitHubTokenInput isGitHubTokenSet={isGitHubTokenSet} />
|
||||
<GitLabTokenInput isGitLabTokenSet={isGitLabTokenSet} />
|
||||
|
||||
<SettingsInput
|
||||
testId="gitlab-token-input"
|
||||
name="gitlab-token-input"
|
||||
label={t(I18nKey.GITLAB$TOKEN_LABEL)}
|
||||
type="password"
|
||||
className="w-[680px]"
|
||||
startContent={
|
||||
isGitLabTokenSet && (
|
||||
<KeyStatusIcon isSet={!!isGitLabTokenSet} />
|
||||
)
|
||||
}
|
||||
placeholder={isGitHubTokenSet ? "<hidden>" : ""}
|
||||
/>
|
||||
|
||||
<p data-testid="gitlab-token-help-anchor" className="text-xs">
|
||||
{" "}
|
||||
{t(I18nKey.GITLAB$GET_TOKEN)}{" "}
|
||||
<b>
|
||||
{" "}
|
||||
<a
|
||||
href="https://gitlab.com/-/user_settings/personal_access_tokens?name=openhands-app&scopes=api,read_user,read_repository,write_repository"
|
||||
target="_blank"
|
||||
className="underline underline-offset-2"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
GitLab
|
||||
</a>{" "}
|
||||
</b>
|
||||
{t(I18nKey.GITLAB$OR_SEE)}{" "}
|
||||
<b>
|
||||
<a
|
||||
href="https://docs.gitlab.com/user/profile/personal_access_tokens/"
|
||||
target="_blank"
|
||||
className="underline underline-offset-2"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{t(I18nKey.COMMON$DOCUMENTATION)}
|
||||
</a>
|
||||
</b>
|
||||
.
|
||||
</p>
|
||||
<BrandButton
|
||||
type="button"
|
||||
variant="secondary"
|
||||
@@ -493,33 +286,11 @@ function AccountSettings() {
|
||||
{t(I18nKey.ACCOUNT_SETTINGS$ADDITIONAL_SETTINGS)}
|
||||
</h2>
|
||||
|
||||
<SettingsDropdownInput
|
||||
testId="language-input"
|
||||
name="language-input"
|
||||
label={t(I18nKey.SETTINGS$LANGUAGE)}
|
||||
items={AvailableLanguages.map((language) => ({
|
||||
key: language.value,
|
||||
label: language.label,
|
||||
}))}
|
||||
defaultSelectedKey={settings.LANGUAGE}
|
||||
isClearable={false}
|
||||
/>
|
||||
|
||||
<SettingsSwitch
|
||||
testId="enable-analytics-switch"
|
||||
name="enable-analytics-switch"
|
||||
defaultIsToggled={!!isAnalyticsEnabled}
|
||||
>
|
||||
{t(I18nKey.ANALYTICS$ENABLE)}
|
||||
</SettingsSwitch>
|
||||
|
||||
<SettingsSwitch
|
||||
testId="enable-sound-notifications-switch"
|
||||
name="enable-sound-notifications-switch"
|
||||
<LanguageInput defaultLanguage={settings.LANGUAGE} />
|
||||
<EnableAnalyticsSwitch defaultIsToggled={!!isAnalyticsEnabled} />
|
||||
<EnableSoundNotificationsSwitch
|
||||
defaultIsToggled={!!settings.ENABLE_SOUND_NOTIFICATIONS}
|
||||
>
|
||||
{t(I18nKey.SETTINGS$SOUND_NOTIFICATIONS)}
|
||||
</SettingsSwitch>
|
||||
/>
|
||||
</section>
|
||||
</div>
|
||||
</form>
|
||||
@@ -544,37 +315,10 @@ function AccountSettings() {
|
||||
</footer>
|
||||
|
||||
{resetSettingsModalIsOpen && (
|
||||
<ModalBackdrop>
|
||||
<div
|
||||
data-testid="reset-modal"
|
||||
className="bg-base-secondary p-4 rounded-xl flex flex-col gap-4 border border-tertiary"
|
||||
>
|
||||
<p>{t(I18nKey.SETTINGS$RESET_CONFIRMATION)}</p>
|
||||
<div className="w-full flex gap-2">
|
||||
<BrandButton
|
||||
type="button"
|
||||
variant="primary"
|
||||
className="grow"
|
||||
onClick={() => {
|
||||
handleReset();
|
||||
}}
|
||||
>
|
||||
Reset
|
||||
</BrandButton>
|
||||
|
||||
<BrandButton
|
||||
type="button"
|
||||
variant="secondary"
|
||||
className="grow"
|
||||
onClick={() => {
|
||||
setResetSettingsModalIsOpen(false);
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</BrandButton>
|
||||
</div>
|
||||
</div>
|
||||
</ModalBackdrop>
|
||||
<ConfirmResetSettingsModal
|
||||
handleReset={handleReset}
|
||||
onClose={() => setResetSettingsModalIsOpen(false)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
90
frontend/src/utils/build-user-preferences.ts
Normal file
90
frontend/src/utils/build-user-preferences.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
import { AvailableLanguages } from "#/i18n";
|
||||
import { DEFAULT_SETTINGS } from "#/services/settings";
|
||||
import { PostSettings } from "#/types/settings";
|
||||
|
||||
const REMOTE_RUNTIME_OPTIONS = [
|
||||
{ key: "1", label: "Standard" },
|
||||
{ key: "2", label: "Enhanced" },
|
||||
{ key: "4", label: "Premium" },
|
||||
];
|
||||
|
||||
/**
|
||||
* Builds user preferences object from form data and configuration flags
|
||||
* @param formData - Form data containing user preferences
|
||||
* @param isLLMKeySet - Flag indicating if LLM key is already set
|
||||
* @param shouldHandleSpecialSaasCase - Flag for special SaaS case handling
|
||||
* @param confirmationModeIsEnabled - Flag for confirmation mode status
|
||||
* @returns User settings configuration object
|
||||
*/
|
||||
export const buildUserPreferences = (
|
||||
formData: FormData,
|
||||
isLLMKeySet: boolean | undefined,
|
||||
shouldHandleSpecialSaasCase: boolean | undefined,
|
||||
confirmationModeIsEnabled: boolean,
|
||||
): Partial<PostSettings> & { user_consents_to_analytics: boolean } => {
|
||||
const languageLabel = formData.get("language-input")?.toString();
|
||||
const languageValue = AvailableLanguages.find(
|
||||
({ label }) => label === languageLabel,
|
||||
)?.value;
|
||||
|
||||
const llmProvider = formData.get("llm-provider-input")?.toString();
|
||||
const llmModel = formData.get("llm-model-input")?.toString();
|
||||
const fullLlmModel = `${llmProvider}/${llmModel}`.toLowerCase();
|
||||
const customLlmModel = formData.get("llm-custom-model-input")?.toString();
|
||||
|
||||
const rawRemoteRuntimeResourceFactor = formData
|
||||
.get("runtime-settings-input")
|
||||
?.toString();
|
||||
const remoteRuntimeResourceFactor = REMOTE_RUNTIME_OPTIONS.find(
|
||||
({ label }) => label === rawRemoteRuntimeResourceFactor,
|
||||
)?.key;
|
||||
|
||||
const userConsentsToAnalytics =
|
||||
formData.get("enable-analytics-switch")?.toString() === "on";
|
||||
const enableMemoryCondenser =
|
||||
formData.get("enable-memory-condenser-switch")?.toString() === "on";
|
||||
const enableSoundNotifications =
|
||||
formData.get("enable-sound-notifications-switch")?.toString() === "on";
|
||||
const llmBaseUrl = formData.get("base-url-input")?.toString().trim() || "";
|
||||
const inputApiKey = formData.get("llm-api-key-input")?.toString() || "";
|
||||
const llmApiKey =
|
||||
inputApiKey === "" && isLLMKeySet
|
||||
? undefined // don't update if it's already set and input is empty
|
||||
: inputApiKey; // otherwise use the input value
|
||||
|
||||
const githubToken = formData.get("github-token-input")?.toString();
|
||||
const gitlabToken = formData.get("gitlab-token-input")?.toString();
|
||||
// we don't want the user to be able to modify these settings in SaaS
|
||||
const finalLlmModel = shouldHandleSpecialSaasCase
|
||||
? undefined
|
||||
: customLlmModel || fullLlmModel;
|
||||
const finalLlmBaseUrl = shouldHandleSpecialSaasCase ? undefined : llmBaseUrl;
|
||||
const finalLlmApiKey = shouldHandleSpecialSaasCase ? undefined : llmApiKey;
|
||||
|
||||
const newSettings = {
|
||||
provider_tokens:
|
||||
githubToken || gitlabToken
|
||||
? {
|
||||
github: githubToken || "",
|
||||
gitlab: gitlabToken || "",
|
||||
}
|
||||
: undefined,
|
||||
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:
|
||||
formData.get("security-analyzer-input")?.toString() || "",
|
||||
REMOTE_RUNTIME_RESOURCE_FACTOR:
|
||||
remoteRuntimeResourceFactor !== null
|
||||
? Number(remoteRuntimeResourceFactor)
|
||||
: DEFAULT_SETTINGS.REMOTE_RUNTIME_RESOURCE_FACTOR,
|
||||
CONFIRMATION_MODE: confirmationModeIsEnabled,
|
||||
};
|
||||
|
||||
return newSettings;
|
||||
};
|
||||
Reference in New Issue
Block a user