Support localization for Nav components (#688)

* init localization

* Update defaul to en

* Fix merge issue and import path.

* Set default to en

* Change jsx to tsx

* Update the password max length string.

* Remove languageContext as using the recoil instead.

* Add localization to component endpoints pages

* Revert default to en after testing.

* Update LoginForm.tsx

* Fix translation.

* Make lint happy

* Merge (#1)

* Create deploy.yml

* Add localization support for endpoint pages components  (#667)

* init localization

* Update defaul to en

* Fix merge issue and import path.

* Set default to en

* Change jsx to tsx

* Update the password max length string.

* Remove languageContext as using the recoil instead.

* Add localization to component endpoints pages

* Revert default to en after testing.

* Update LoginForm.tsx

* Fix translation.

* Make lint happy

* Add a restart to melisearch in docker-compose.yml (#684)

* Oauth fixes for Cognito (#686)

* Add a restart to melisearch in docker-compose.yml

* Oauth fixes for Cognito

* Use the username or email for full name from oath if not provided

---------

Co-authored-by: Donavan <snark@hey.com>

* Italian localization support for endpoint (#687)

---------

Co-authored-by: Danny Avila <110412045+danny-avila@users.noreply.github.com>
Co-authored-by: Donavan Stanley <donavan.stanley@gmail.com>
Co-authored-by: Donavan <snark@hey.com>
Co-authored-by: Marco Beretta <81851188+Berry-13@users.noreply.github.com>

* Translate Nav pages

* Fix npm test

---------

Co-authored-by: Danny Avila <110412045+danny-avila@users.noreply.github.com>
Co-authored-by: Donavan Stanley <donavan.stanley@gmail.com>
Co-authored-by: Donavan <snark@hey.com>
Co-authored-by: Marco Beretta <81851188+Berry-13@users.noreply.github.com>
This commit is contained in:
Abner Chou 2023-07-24 08:33:08 -04:00 committed by GitHub
parent 3b865fbc59
commit 1e49b7ecb1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
22 changed files with 346 additions and 130 deletions

View file

@ -55,13 +55,15 @@ function Registration() {
return (
<div className="flex min-h-screen flex-col items-center justify-center bg-white pt-6 sm:pt-0">
<div className="mt-6 w-96 overflow-hidden bg-white px-6 py-4 sm:max-w-md sm:rounded-lg">
<h1 className="mb-4 text-center text-3xl font-semibold">Create your account</h1>
<h1 className="mb-4 text-center text-3xl font-semibold">
{localize(lang, 'com_auth_create_account')}
</h1>
{error && (
<div
className="relative mt-4 rounded border border-red-400 bg-red-100 px-4 py-3 text-red-700"
role="alert"
>
There was an error attempting to register your account. Please try again. {errorMessage}
{localize(lang, 'com_auth_error_create')} {errorMessage}
</div>
)}
<form

View file

@ -169,7 +169,7 @@ const EditPresetDialog = ({ open, onOpenChange, preset: _preset, title }) => {
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogTemplate
title={`${title || 'Edit Preset'} - ${preset?.title}`}
title={`${title || localize(lang, 'com_endpoint_edit_preset')} - ${preset?.title}`}
className="h-[675px] max-w-full sm:max-w-4xl "
main={
<div className="flex w-full flex-col items-center gap-2 md:h-[475px]">

View file

@ -48,7 +48,7 @@ const EndpointOptionsDialog = ({ open, onOpenChange, preset: _preset, title }) =
<>
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogTemplate
title={`${title || 'View Options'} - ${endpointName}`}
title={`${title || localize(lang, 'com_endpoint_view_options')} - ${endpointName}`}
className="max-w-full sm:max-w-4xl"
main={
<div className="flex w-full flex-col items-center gap-2">

View file

@ -46,7 +46,7 @@ function Settings(props) {
<div className="col-span-1 flex flex-col items-center justify-start gap-6">
<div className="grid w-full items-center gap-2">
<SelectDropDown
title="Agent Model (Recommended: GPT-3.5)"
title={localize(lang, 'com_endpoint_agent_model')}
value={model}
setValue={setModel}
availableValues={models}
@ -102,7 +102,10 @@ function Settings(props) {
<HoverCardTrigger className="grid w-full items-center gap-2">
<div className="flex justify-between">
<Label htmlFor="temp-int" className="text-left text-sm font-medium">
{localize(lang, 'com_endpoint_temperature')} <small className="opacity-40">({localize(lang, 'com_endpoint_default')}: 0)</small>
{localize(lang, 'com_endpoint_temperature')}{' '}
<small className="opacity-40">
({localize(lang, 'com_endpoint_default')}: 0)
</small>
</Label>
<InputNumber
id="temp-int"

View file

@ -1,20 +1,20 @@
import { HoverCardPortal, HoverCardContent } from '~/components';
import { useRecoilValue } from 'recoil';
import store from '~/store';
import { localize } from '~/localization/Translation';
const types = {
temp: 'Higher values = more random, while lower values = more focused and deterministic. We recommend altering this or Top P but not both.',
func: 'Enable use of Plugins as OpenAI Functions',
skip: 'Enable skipping the completion step, which reviews the final answer and generated steps',
max: 'The max tokens to generate. The total length of input tokens and generated tokens is limited by the model\'s context length.',
topp: 'An alternative to sampling with temperature, called nucleus sampling, where the model considers the results of the tokens with top_p probability mass. So 0.1 means only the tokens comprising the top 10% probability mass are considered. We recommend altering this or temperature but not both.',
freq: 'Number between -2.0 and 2.0. Positive values penalize new tokens based on their existing frequency in the text so far, decreasing the model\'s likelihood to repeat the same line verbatim.',
pres: 'Number between -2.0 and 2.0. Positive values penalize new tokens based on whether they appear in the text so far, increasing the model\'s likelihood to talk about new topics.',
temp: 'com_endpoint_openai_temp',
func: 'com_endpoint_func_hover',
skip: 'com_endpoint_skip_hover',
max: 'com_endpoint_openai_max',
topp: 'com_endpoint_openai_topp',
freq: 'com_endpoint_openai_freq',
pres: 'com_endpoint_openai_pres',
};
function OptionHover({ type, side }) {
// const options = {};
// if (type === 'pres') {
// options.sideOffset = 45;
// }
const lang = useRecoilValue(store.lang);
return (
<HoverCardPortal>
@ -24,7 +24,7 @@ function OptionHover({ type, side }) {
// {...options}
>
<div className="space-y-2">
<p className="text-sm text-gray-600 dark:text-gray-300">{types[type]}</p>
<p className="text-sm text-gray-600 dark:text-gray-300">{localize(lang, types[type])}</p>
</div>
</HoverCardContent>
</HoverCardPortal>

View file

@ -55,7 +55,7 @@ function Settings(props) {
<div className="col-span-1 flex flex-col items-center justify-start gap-6">
<div className="grid w-full items-center gap-2">
<SelectDropDown
title="Completion Model (Recommended: GPT-4)"
title={localize(lang, 'com_endpoint_completion_model')}
value={model}
setValue={setModel}
availableValues={models}
@ -71,7 +71,10 @@ function Settings(props) {
<div className="grid w-full items-center gap-2">
<Label htmlFor="chatGptLabel" className="text-left text-sm font-medium">
{localize(lang, 'com_endpoint_custom_name')}{' '}
<small className="opacity-40">({localize(lang, 'com_endpoint_default_empty')} | {localize(lang, 'com_endpoint_disabled_with_tools')})</small>
<small className="opacity-40">
({localize(lang, 'com_endpoint_default_empty')} |{' '}
{localize(lang, 'com_endpoint_disabled_with_tools')})
</small>
</Label>
<Input
id="chatGptLabel"
@ -81,7 +84,7 @@ function Settings(props) {
placeholder={
toolsSelected
? localize(lang, 'com_endpoint_disabled_with_tools_placeholder')
: localize(lang, 'com_endpoint_plug_set_custom_name_for_gpt_placeholder')
: localize(lang, 'com_endpoint_openai_custom_name_placeholder')
}
className={cn(
defaultTextProps,
@ -92,7 +95,10 @@ function Settings(props) {
<div className="grid w-full items-center gap-2">
<Label htmlFor="promptPrefix" className="text-left text-sm font-medium">
{localize(lang, 'com_endpoint_prompt_prefix')}{' '}
<small className="opacity-40">({localize(lang, 'com_endpoint_default_empty')} | {localize(lang, 'com_endpoint_disabled_with_tools')})</small>
<small className="opacity-40">
({localize(lang, 'com_endpoint_default_empty')} |{' '}
{localize(lang, 'com_endpoint_disabled_with_tools')})
</small>
</Label>
<TextareaAutosize
id="promptPrefix"
@ -102,7 +108,10 @@ function Settings(props) {
placeholder={
toolsSelected
? localize(lang, 'com_endpoint_disabled_with_tools_placeholder')
: localize(lang, 'com_endpoint_plug_set_custom_instructions_for_gpt_placeholder')
: localize(
lang,
'com_endpoint_plug_set_custom_instructions_for_gpt_placeholder',
)
}
className={cn(
defaultTextProps,
@ -117,7 +126,10 @@ function Settings(props) {
<HoverCardTrigger className="grid w-full items-center gap-2">
<div className="flex justify-between">
<Label htmlFor="temp-int" className="text-left text-sm font-medium">
Temperature <small className="opacity-40">{'(default: 0.8)'}</small>
{localize(lang, 'com_endpoint_temperature')}{' '}
<small className="opacity-40">
({localize(lang, 'com_endpoint_default_with_num', 0.8)})
</small>
</Label>
<InputNumber
id="temp-int"
@ -154,7 +166,10 @@ function Settings(props) {
<HoverCardTrigger className="grid w-full items-center gap-2">
<div className="flex justify-between">
<Label htmlFor="top-p-int" className="text-left text-sm font-medium">
{localize(lang, 'com_endpoint_top_p')} <small className="opacity-40">({localize(lang, 'com_endpoint_default_with_num', 1)})</small>
{localize(lang, 'com_endpoint_top_p')}{' '}
<small className="opacity-40">
({localize(lang, 'com_endpoint_default_with_num', 1)})
</small>
</Label>
<InputNumber
id="top-p-int"
@ -192,7 +207,10 @@ function Settings(props) {
<HoverCardTrigger className="grid w-full items-center gap-2">
<div className="flex justify-between">
<Label htmlFor="freq-penalty-int" className="text-left text-sm font-medium">
{localize(lang, 'com_endpoint_frequency_penalty')} <small className="opacity-40">({localize(lang, 'com_endpoint_default_with_num', 0)})</small>
{localize(lang, 'com_endpoint_frequency_penalty')}{' '}
<small className="opacity-40">
({localize(lang, 'com_endpoint_default_with_num', 0)})
</small>
</Label>
<InputNumber
id="freq-penalty-int"
@ -230,7 +248,10 @@ function Settings(props) {
<HoverCardTrigger className="grid w-full items-center gap-2">
<div className="flex justify-between">
<Label htmlFor="pres-penalty-int" className="text-left text-sm font-medium">
{localize(lang, 'com_endpoint_presence_penalty')} <small className="opacity-40">({localize(lang, 'com_endpoint_default_with_num', 0)})</small>
{localize(lang, 'com_endpoint_presence_penalty')}{' '}
<small className="opacity-40">
({localize(lang, 'com_endpoint_default_with_num', 0)})
</small>
</Label>
<InputNumber
id="pres-penalty-int"

View file

@ -28,14 +28,14 @@ const SaveAsPresetDialog = ({ open, onOpenChange, preset }) => {
};
useEffect(() => {
setTitle(preset?.title || 'My Preset');
setTitle(preset?.title || localize(lang, 'com_endpoint_my_preset'));
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [open]);
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogTemplate
title="Save As Preset"
title={localize(lang, 'com_endpoint_save_as_preset')}
main={
<div className="grid w-full items-center gap-2">
<Label htmlFor="chatGptLabel" className="text-left text-sm font-medium">

View file

@ -3,12 +3,15 @@ import { Dialog, DialogTemplate } from '../ui/';
import { ClearChatsButton } from './SettingsTabs/';
import { useClearConversationsMutation } from '@librechat/data-provider';
import store from '~/store';
import { useRecoilValue } from 'recoil';
import { localize } from '~/localization/Translation';
const ClearConvos = ({ open, onOpenChange }) => {
const { newConversation } = store.useConversation();
const { refreshConversations } = store.useConversations();
const clearConvosMutation = useClearConversationsMutation();
const [confirmClear, setConfirmClear] = useState(false);
const lang = useRecoilValue(store.lang);
const clearConvos = useCallback(() => {
if (confirmClear) {
@ -30,8 +33,8 @@ const ClearConvos = ({ open, onOpenChange }) => {
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogTemplate
title="Clear conversations"
description="Are you sure you want to clear all conversations? This is irreversible."
title={localize(lang, 'com_nav_clear_conversation')}
description={localize(lang, 'com_nav_clear_conversation_confirm_message')}
leftButtons={
<ClearChatsButton showText={false} confirmClear={confirmClear} onClick={clearConvos} />
}

View file

@ -17,6 +17,7 @@ import { useScreenshot } from '~/utils/screenshotContext';
import store from '~/store';
import cleanupPreset from '~/utils/cleanupPreset.js';
import { localize } from '~/localization/Translation';
export default function ExportModel({ open, onOpenChange }) {
const { captureScreenshot } = useScreenshot();
@ -32,6 +33,8 @@ export default function ExportModel({ open, onOpenChange }) {
const messagesTree = useRecoilValue(store.messagesTree) || [];
const endpointsConfig = useRecoilValue(store.endpointsConfig);
const lang = useRecoilValue(store.lang);
const getSiblingIdx = useRecoilCallback(
({ snapshot }) =>
async (messageId) =>
@ -349,13 +352,13 @@ export default function ExportModel({ open, onOpenChange }) {
<div className="grid w-full gap-6 sm:grid-cols-2">
<div className="col-span-1 flex flex-col items-start justify-start gap-2">
<Label htmlFor="filename" className="text-left text-sm font-medium">
Filename
{localize(lang, 'com_nav_export_filename')}
</Label>
<Input
id="filename"
value={filename}
onChange={(e) => setFileName(filenamify(e.target.value || ''))}
placeholder="Set the filename"
placeholder={localize(lang, 'com_nav_export_filename_placeholder')}
className={cn(
defaultTextProps,
'flex h-10 max-h-10 w-full resize-none px-3 py-2 focus:outline-none focus:ring-0 focus:ring-opacity-0 focus:ring-offset-0',
@ -364,7 +367,7 @@ export default function ExportModel({ open, onOpenChange }) {
</div>
<div className="col-span-1 flex flex-col items-start justify-start gap-2">
<Label htmlFor="type" className="text-left text-sm font-medium">
Type
{localize(lang, 'com_nav_export_type')}
</Label>
<Dropdown
id="type"
@ -383,7 +386,7 @@ export default function ExportModel({ open, onOpenChange }) {
<div className="col-span-1 flex flex-col items-start justify-start gap-2">
<div className="grid w-full items-center gap-2">
<Label htmlFor="includeOptions" className="text-left text-sm font-medium">
Include endpoint options
{localize(lang, 'com_nav_export_include_endpoint_options')}
</Label>
<div className="flex h-[40px] w-full items-center space-x-3">
<Checkbox
@ -397,14 +400,16 @@ export default function ExportModel({ open, onOpenChange }) {
htmlFor="includeOptions"
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70 dark:text-gray-50"
>
{exportOptionsSupport ? 'Enabled' : 'Not Supported'}
{exportOptionsSupport
? localize(lang, 'com_nav_enabled')
: localize(lang, 'com_nav_not_supported')}
</label>
</div>
</div>
</div>
<div className="grid w-full items-center gap-2">
<Label htmlFor="exportBranches" className="text-left text-sm font-medium">
Export all message branches
{localize(lang, 'com_nav_export_all_message_branches')}
</Label>
<div className="flex h-[40px] w-full items-center space-x-3">
<Checkbox
@ -418,14 +423,16 @@ export default function ExportModel({ open, onOpenChange }) {
htmlFor="exportBranches"
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70 dark:text-gray-50"
>
{exportBranchesSupport ? 'Enabled' : 'Not Supported'}
{exportBranchesSupport
? localize(lang, 'com_nav_enabled')
: localize(lang, 'com_nav_not_supported')}
</label>
</div>
</div>
{type === 'json' ? (
<div className="grid w-full items-center gap-2">
<Label htmlFor="recursive" className="text-left text-sm font-medium">
Recursive or sequential?
{localize(lang, 'com_nav_export_recursive_or_sequential')}
</Label>
<div className="flex h-[40px] w-full items-center space-x-3">
<Checkbox
@ -438,7 +445,7 @@ export default function ExportModel({ open, onOpenChange }) {
htmlFor="recursive"
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70 dark:text-gray-50"
>
Recursive
{localize(lang, 'com_nav_export_recursive')}
</label>
</div>
</div>
@ -452,7 +459,7 @@ export default function ExportModel({ open, onOpenChange }) {
onClick={exportConversation}
className="dark:hover:gray-400 border-gray-700 bg-green-600 text-white hover:bg-green-700 dark:hover:bg-green-800"
>
Export
{localize(lang, 'com_endpoint_export')}
</DialogButton>
</>
}

View file

@ -6,9 +6,11 @@ import { cn } from '~/utils/';
import ExportModel from './ExportModel';
import store from '~/store';
import { localize } from '~/localization/Translation';
const ExportConversation = forwardRef(() => {
const [open, setOpen] = useState(false);
const lang = useRecoilValue(store.lang);
const conversation = useRecoilValue(store.conversation) || {};
@ -33,7 +35,7 @@ const ExportConversation = forwardRef(() => {
onClick={clickHandler}
>
<Download size={16} />
Export conversation
{localize(lang, 'com_nav_export_conversation')}
</button>
<ExportModel open={open} onOpenChange={setOpen} />

View file

@ -1,9 +1,13 @@
import { forwardRef } from 'react';
import LogOutIcon from '../svg/LogOutIcon';
import { useAuthContext } from '~/hooks/AuthContext';
import { useRecoilValue } from 'recoil';
import store from '~/store';
import { localize } from '~/localization/Translation';
const Logout = forwardRef(() => {
const { user, logout } = useAuthContext();
const lang = useRecoilValue(store.lang);
const handleLogout = () => {
logout();
@ -16,8 +20,8 @@ const Logout = forwardRef(() => {
onClick={handleLogout}
>
<LogOutIcon />
{user?.username || 'USER'}
<small>Log out</small>
{user?.username || localize(lang, 'com_nav_user')}
<small>{localize(lang, 'com_nav_log_out')}</small>
</button>
);
});

View file

@ -2,11 +2,13 @@ import React from 'react';
import { useRecoilValue } from 'recoil';
import store from '~/store';
import { localize } from '~/localization/Translation';
export default function MobileNav({ setNavVisible }) {
const conversation = useRecoilValue(store.conversation);
const { newConversation } = store.useConversation();
const { title = 'New Chat' } = conversation || {};
const lang = useRecoilValue(store.lang);
return (
<div className="fixed left-0 right-0 top-0 z-10 flex items-center border-b border-white/20 bg-gray-800 pl-1 pt-1 text-gray-200 sm:pl-3 md:hidden">
@ -15,7 +17,7 @@ export default function MobileNav({ setNavVisible }) {
className="-ml-0.5 -mt-0.5 inline-flex h-10 w-10 items-center justify-center rounded-md hover:text-gray-900 focus:outline-none focus:ring-0 focus:ring-inset focus:ring-white dark:hover:text-white"
onClick={() => setNavVisible((prev) => !prev)}
>
<span className="sr-only">Open sidebar</span>
<span className="sr-only">{localize(lang, 'com_nav_open_sidebar')}</span>
<svg
stroke="currentColor"
fill="none"
@ -33,7 +35,9 @@ export default function MobileNav({ setNavVisible }) {
<line x1="3" y1="18" x2="21" y2="18" />
</svg>
</button>
<h1 className="flex-1 text-center text-base font-normal">{title || 'New Chat'}</h1>
<h1 className="flex-1 text-center text-base font-normal">
{title || localize(lang, 'com_ui_new_chat')}
</h1>
<button type="button" className="px-3" onClick={() => newConversation()}>
<svg
stroke="currentColor"

View file

@ -13,12 +13,14 @@ import { cn } from '~/utils/';
import store from '~/store';
import { LinkIcon, DotsIcon, GearIcon, TrashIcon } from '~/components';
import { localize } from '~/localization/Translation';
export default function NavLinks({ clearSearch, isSearchEnabled }) {
const [showExports, setShowExports] = useState(false);
const [showClearConvos, setShowClearConvos] = useState(false);
const [showSettings, setShowSettings] = useState(false);
const { user } = useAuthContext();
const lang = useRecoilValue(store.lang);
const conversation = useRecoilValue(store.conversation) || {};
@ -59,7 +61,7 @@ export default function NavLinks({ clearSearch, isSearchEnabled }) {
</div>
</div>
<div className="grow overflow-hidden text-ellipsis whitespace-nowrap text-left text-white">
{user?.name || 'USER'}
{user?.name || localize(lang, 'com_nav_user')}
</div>
<DotsIcon />
</Menu.Button>
@ -86,7 +88,7 @@ export default function NavLinks({ clearSearch, isSearchEnabled }) {
exportable ? 'cursor-pointer text-white' : 'cursor-not-allowed text-white/50',
)}
svg={() => <Download size={16} />}
text="Export conversation"
text={localize(lang, 'com_nav_export_conversation')}
clickHandler={clickHandler}
/>
</Menu.Item>
@ -95,7 +97,7 @@ export default function NavLinks({ clearSearch, isSearchEnabled }) {
<NavLink
className="flex w-full cursor-pointer items-center gap-3 rounded-none px-3 py-3 text-sm text-white transition-colors duration-200 hover:bg-gray-700"
svg={() => <TrashIcon />}
text="Clear conversations"
text={localize(lang, 'com_nav_clear_conversation')}
clickHandler={() => setShowClearConvos(true)}
/>
</Menu.Item>
@ -103,7 +105,7 @@ export default function NavLinks({ clearSearch, isSearchEnabled }) {
<NavLink
className="flex w-full cursor-pointer items-center gap-3 rounded-none px-3 py-3 text-sm text-white transition-colors duration-200 hover:bg-gray-700"
svg={() => <LinkIcon />}
text="Help & FAQ"
text={localize(lang, 'com_nav_help_faq')}
clickHandler={() => window.open('https://docs.librechat.ai/', '_blank')}
/>
</Menu.Item>
@ -111,7 +113,7 @@ export default function NavLinks({ clearSearch, isSearchEnabled }) {
<NavLink
className="flex w-full cursor-pointer items-center gap-3 rounded-none px-3 py-3 text-sm text-white transition-colors duration-200 hover:bg-gray-700"
svg={() => <GearIcon />}
text="Settings"
text={localize(lang, 'com_nav_settings')}
clickHandler={() => setShowSettings(true)}
/>
</Menu.Item>

View file

@ -1,12 +1,14 @@
import { forwardRef, useState, useEffect } from 'react';
import { Search, X } from 'lucide-react';
import { useRecoilState } from 'recoil';
import { useRecoilState, useRecoilValue } from 'recoil';
import store from '~/store';
import { localize } from '~/localization/Translation';
const SearchBar = forwardRef((props, ref) => {
const { clearSearch } = props;
const [searchQuery, setSearchQuery] = useRecoilState(store.searchQuery);
const [showClearIcon, setShowClearIcon] = useState(false);
const lang = useRecoilValue(store.lang);
const handleKeyUp = (e) => {
const { value } = e.target;
@ -44,7 +46,7 @@ const SearchBar = forwardRef((props, ref) => {
onKeyDown={(e) => {
e.code === 'Space' ? e.stopPropagation() : null;
}}
placeholder="Search messages"
placeholder={localize(lang, 'com_nav_search_placeholder')}
onKeyUp={handleKeyUp}
/>
<X

View file

@ -5,7 +5,9 @@ import { CogIcon } from '~/components/svg';
import { useEffect, useState } from 'react';
import { cn } from '~/utils/';
import { useClearConversationsMutation } from '@librechat/data-provider';
import { useRecoilValue } from 'recoil';
import store from '~/store';
import { localize } from '~/localization/Translation';
export default function Settings({ open, onOpenChange }) {
const { newConversation } = store.useConversation();
@ -13,6 +15,7 @@ export default function Settings({ open, onOpenChange }) {
const clearConvosMutation = useClearConversationsMutation();
const [confirmClear, setConfirmClear] = useState(false);
const [isMobile, setIsMobile] = useState(false);
const lang = useRecoilValue(store.lang);
// check if mobile dynamically and update
useEffect(() => {
@ -56,7 +59,7 @@ export default function Settings({ open, onOpenChange }) {
<DialogContent className={cn('shadow-2xl dark:bg-gray-900 dark:text-white')}>
<DialogHeader>
<DialogTitle className="text-lg font-medium leading-6 text-gray-900 dark:text-gray-200">
Settings
{localize(lang, 'com_nav_settings')}
</DialogTitle>
</DialogHeader>
<div className="px-6">
@ -84,7 +87,7 @@ export default function Settings({ open, onOpenChange }) {
value="general"
>
<CogIcon />
General
{localize(lang, 'com_nav_setting_general')}
</Tabs.Trigger>
</Tabs.List>
<General />

View file

@ -2,6 +2,7 @@ import React from 'react';
import { render, fireEvent } from '@testing-library/react';
import '@testing-library/jest-dom/extend-expect';
import { ClearChatsButton } from './General';
import { RecoilRoot } from 'recoil';
describe('ClearChatsButton', () => {
let mockOnClick;
@ -12,7 +13,9 @@ describe('ClearChatsButton', () => {
it('renders correctly', () => {
const { getByText } = render(
<ClearChatsButton confirmClear={false} showText={true} onClick={mockOnClick} />,
<RecoilRoot>
<ClearChatsButton confirmClear={false} showText={true} onClick={mockOnClick} />
</RecoilRoot>,
);
expect(getByText('Clear all chats')).toBeInTheDocument();
@ -21,7 +24,9 @@ describe('ClearChatsButton', () => {
it('renders confirm clear when confirmClear is true', () => {
const { getByText } = render(
<ClearChatsButton confirmClear={true} showText={true} onClick={mockOnClick} />,
<RecoilRoot>
<ClearChatsButton confirmClear={true} showText={true} onClick={mockOnClick} />
</RecoilRoot>,
);
expect(getByText('Confirm Clear')).toBeInTheDocument();
@ -29,7 +34,9 @@ describe('ClearChatsButton', () => {
it('calls onClick when the button is clicked', () => {
const { getByText } = render(
<ClearChatsButton confirmClear={false} showText={true} onClick={mockOnClick} />,
<RecoilRoot>
<ClearChatsButton confirmClear={false} showText={true} onClick={mockOnClick} />
</RecoilRoot>,
);
fireEvent.click(getByText('Clear'));

View file

@ -3,6 +3,9 @@ import { CheckIcon } from 'lucide-react';
import { ThemeContext } from '~/hooks/ThemeContext';
import React, { useState, useContext, useCallback } from 'react';
import { useClearConversationsMutation } from '@librechat/data-provider';
import { useRecoilValue } from 'recoil';
import store from '~/store';
import { localize } from '~/localization/Translation';
export const ThemeSelector = ({
theme,
@ -10,20 +13,24 @@ export const ThemeSelector = ({
}: {
theme: string;
onChange: (value: string) => void;
}) => (
<div className="flex items-center justify-between">
<div>Theme</div>
<select
className="w-24 rounded border border-black/10 bg-transparent text-sm dark:border-white/20 dark:bg-gray-900"
onChange={(e) => onChange(e.target.value)}
value={theme}
>
<option value="system">System</option>
<option value="dark">Dark</option>
<option value="light">Light</option>
</select>
</div>
);
}) => {
const lang = useRecoilValue(store.lang);
return (
<div className="flex items-center justify-between">
<div>{localize(lang, 'com_nav_theme')}</div>
<select
className="w-24 rounded border border-black/10 bg-transparent text-sm dark:border-white/20 dark:bg-gray-900"
onChange={(e) => onChange(e.target.value)}
value={theme}
>
<option value="system">{localize(lang, 'com_nav_theme_system')}</option>
<option value="dark">{localize(lang, 'com_nav_theme_dark')}</option>
<option value="light">{localize(lang, 'com_nav_theme_light')}</option>
</select>
</div>
);
};
export const ClearChatsButton = ({
confirmClear,
@ -33,27 +40,31 @@ export const ClearChatsButton = ({
confirmClear: boolean;
showText: boolean;
onClick: () => void;
}) => (
<div className="flex items-center justify-between">
{showText && <div>Clear all chats</div>}
<button
className="btn relative bg-red-600 text-white hover:bg-red-800"
type="button"
id="clearConvosBtn"
onClick={onClick}
>
{confirmClear ? (
<div className="flex w-full items-center justify-center gap-2" id="clearConvosTxt">
<CheckIcon className="h-5 w-5" /> Confirm Clear
</div>
) : (
<div className="flex w-full items-center justify-center gap-2" id="clearConvosTxt">
Clear
</div>
)}
</button>
</div>
);
}) => {
const lang = useRecoilValue(store.lang);
return (
<div className="flex items-center justify-between">
{showText && <div>{localize(lang, 'com_nav_clear_all_chats')}</div>}
<button
className="btn relative bg-red-600 text-white hover:bg-red-800"
type="button"
id="clearConvosBtn"
onClick={onClick}
>
{confirmClear ? (
<div className="flex w-full items-center justify-center gap-2" id="clearConvosTxt">
<CheckIcon className="h-5 w-5" /> {localize(lang, 'com_nav_confirm_clear')}
</div>
) : (
<div className="flex w-full items-center justify-center gap-2" id="clearConvosTxt">
{localize(lang, 'com_nav_clear')}
</div>
)}
</button>
</div>
);
};
function General() {
const { theme, setTheme } = useContext(ThemeContext);

View file

@ -2,6 +2,7 @@ import React from 'react';
import { render, fireEvent } from '@testing-library/react';
import '@testing-library/jest-dom/extend-expect';
import { ThemeSelector } from './General';
import { RecoilRoot } from 'recoil';
describe('ThemeSelector', () => {
let mockOnChange;
@ -12,7 +13,9 @@ describe('ThemeSelector', () => {
it('renders correctly', () => {
const { getByText, getByDisplayValue } = render(
<ThemeSelector theme="system" onChange={mockOnChange} />,
<RecoilRoot>
<ThemeSelector theme="system" onChange={mockOnChange} />
</RecoilRoot>,
);
expect(getByText('Theme')).toBeInTheDocument();
@ -20,7 +23,11 @@ describe('ThemeSelector', () => {
});
it('calls onChange when the select value changes', () => {
const { getByDisplayValue } = render(<ThemeSelector theme="system" onChange={mockOnChange} />);
const { getByDisplayValue } = render(
<RecoilRoot>
<ThemeSelector theme="system" onChange={mockOnChange} />
</RecoilRoot>,
);
fireEvent.change(getByDisplayValue('System'), { target: { value: 'dark' } });

View file

@ -10,12 +10,14 @@ import { Panel, Spinner } from '~/components';
import { cn } from '~/utils/';
import { useAuthContext, useDebounce } from '~/hooks';
import store from '~/store';
import { localize } from '~/localization/Translation';
export default function Nav({ navVisible, setNavVisible }) {
const [isHovering, setIsHovering] = useState(false);
const { isAuthenticated } = useAuthContext();
const containerRef = useRef(null);
const scrollPositionRef = useRef(null);
const lang = useRecoilValue(store.lang);
const [conversations, setConversations] = useState([]);
// current page
@ -151,7 +153,7 @@ export default function Nav({ navVisible, setNavVisible }) {
)}
onClick={toggleNavVisible}
>
<span className="sr-only">Close sidebar</span>
<span className="sr-only">{localize(lang, 'com_nav_close_sidebar')}</span>
<Panel open={false} />
</button>
</div>
@ -195,7 +197,7 @@ export default function Nav({ navVisible, setNavVisible }) {
onClick={toggleNavVisible}
>
<div className="flex items-center justify-center">
<span className="sr-only">Open sidebar</span>
<span className="sr-only">{localize(lang, 'com_nav_open_sidebar')}</span>
<Panel open={true} />
</div>
</button>

View file

@ -42,6 +42,11 @@ export const localize = (langCode: string, phraseKey: string, ...values: string[
return lang[phraseKey].format(...values);
}
// Fall back logic to cover untranslated phrases
return English[phraseKey].format(...values);
if (phraseKey in English) {
// Fall back logic to cover untranslated phrases
return English[phraseKey].format(...values);
}
// In case the key is not defined, return empty instead of throw errors.
return '';
};

View file

@ -132,7 +132,6 @@ export default {
com_endpoint_plug_skip_completion: 'Skip Completion',
com_endpoint_disabled_with_tools: 'disabled with tools',
com_endpoint_disabled_with_tools_placeholder: 'Disabled with Tools Selected',
com_endpoint_plug_set_custom_name_for_gpt_placeholder: 'Set a custom name for ChatGPT.',
com_endpoint_plug_set_custom_instructions_for_gpt_placeholder:
'Set custom instructions to include in System Message. Default: none',
com_endpoint_set_custom_name: 'Set a custom name, in case you can find this preset',
@ -148,4 +147,40 @@ export default {
com_endpoint_export: 'Export',
com_endpoint_save_as_preset: 'Save As Preset',
com_endpoint_not_implemented: 'Not implemented',
com_endpoint_edit_preset: 'Edit Preset',
com_endpoint_view_options: 'View Options',
com_endpoint_my_preset: 'My Preset',
com_endpoint_agent_model: 'Agent Model (Recommended: GPT-3.5)',
com_endpoint_completion_model: 'Completion Model (Recommended: GPT-4)',
com_endpoint_func_hover: 'Enable use of Plugins as OpenAI Functions',
com_endpoint_skip_hover:
'Enable skipping the completion step, which reviews the final answer and generated steps',
com_nav_export_filename: 'Filename',
com_nav_export_filename_placeholder: 'Set the filename',
com_nav_export_type: 'Type',
com_nav_export_include_endpoint_options: 'Include endpoint options',
com_nav_enabled: 'Enabled',
com_nav_not_supported: 'Not Supported',
com_nav_export_all_message_branches: 'Export all message branches',
com_nav_export_recursive_or_sequential: 'Recursive or sequential?',
com_nav_export_recursive: 'Recursive',
com_nav_export_conversation: 'Export conversation',
com_nav_theme: 'Theme',
com_nav_theme_system: 'System',
com_nav_theme_dark: 'Dark',
com_nav_theme_light: 'Light',
com_nav_clear: 'Clear',
com_nav_clear_all_chats: 'Clear all chats',
com_nav_confirm_clear: 'Confirm Clear',
com_nav_close_sidebar: 'Close sidebar',
com_nav_open_sidebar: 'Open sidebar',
com_nav_log_out: 'Log out',
com_nav_user: 'USER',
com_nav_clear_conversation: 'Clear conversations',
com_nav_clear_conversation_confirm_message:
'Are you sure you want to clear all conversations? This is irreversible.',
com_nav_help_faq: 'Help & FAQ',
com_nav_settings: 'Settings',
com_nav_search_placeholder: 'Search messages',
com_nav_setting_general: 'General',
};

View file

@ -3,18 +3,17 @@
export default {
com_ui_examples: '例子',
com_ui_new_chat: '新对话',
com_ui_example_quantum_computing: 'Explain quantum computing in simple terms',
com_ui_example_10_year_old_b_day: 'Got any creative ideas for a 10 year old\'s birthday?',
com_ui_example_http_in_js: 'How do I make an HTTP request in Javascript?',
com_ui_example_quantum_computing: '如何给7岁小孩讲解量子计算',
com_ui_example_10_year_old_b_day: '如何举办生日宴才能耳目一新?',
com_ui_example_http_in_js: '如何在Python中实现HTTP请求',
com_ui_capabilities: '特点',
com_ui_capability_remember: 'Remembers what user said earlier in the conversation',
com_ui_capability_correction: 'Allows user to provide follow-up corrections',
com_ui_capability_decline_requests: 'Trained to decline inappropriate requests',
com_ui_capability_remember: '历史对话记录',
com_ui_capability_correction: '实时提供反馈',
com_ui_capability_decline_requests: '阻止非法请求',
com_ui_limitations: '限制',
com_ui_limitation_incorrect_info: 'May occasionally generate incorrect information',
com_ui_limitation_harmful_biased:
'May occasionally produce harmful instructions or biased content',
com_ui_limitation_limited_2021: 'Limited knowledge of world and events after 2021',
com_ui_limitation_incorrect_info: '可能会不时出现错误信息',
com_ui_limitation_harmful_biased: '可能会提供有害指示或者偏见',
com_ui_limitation_limited_2021: '训练基于2021年以前信息',
com_ui_input: '输入',
com_ui_close: '关闭',
com_ui_model: '模型',
@ -29,9 +28,8 @@ export default {
com_ui_hide_prompt_templates: '隐藏对话模板',
com_ui_showing: '显示',
com_ui_of: '/',
com_ui_entries: 'Entries',
com_auth_error_login:
'Unable to login with the information provided. Please check your credentials and try again.',
com_ui_entries: '项',
com_auth_error_login: '无法登录,请确认提供的账户密码正确,并重新尝试。',
com_auth_no_account: '新用户注册',
com_auth_sign_up: '注册',
com_auth_sign_in: '登录',
@ -39,43 +37,141 @@ export default {
com_auth_github_login: 'Github登录',
com_auth_discord_login: 'Discord登录',
com_auth_email: '电子邮箱',
com_auth_email_required: 'Email is required',
com_auth_email_min_length: 'Email must be at least 6 characters',
com_auth_email_max_length: 'Email should not be longer than 120 characters',
com_auth_email_pattern: 'You must enter a valid email address',
com_auth_email_required: '邮箱为必填项',
com_auth_email_min_length: '邮箱地址至少6个字符',
com_auth_email_max_length: '邮箱地址最多120个字符',
com_auth_email_pattern: '请输入正确的电子邮箱格式',
com_auth_email_address: '电子邮箱地址',
com_auth_password: '密码',
com_auth_password_required: 'Password is required',
com_auth_password_min_length: 'Password must be at least 8 characters',
com_auth_password_max_length: 'Password must be less than 128 characters',
com_auth_password_required: '密码为必填项',
com_auth_password_min_length: '密码至少8个字符',
com_auth_password_max_length: '密码最多128个字符',
com_auth_password_forgot: '忘记密码?',
com_auth_password_confirm: '确认密码',
com_auth_password_not_match: '密码不一致',
com_auth_continue: '继续',
com_auth_create_account: '创建账号',
com_auth_error_create:
'There was an error attempting to register your account. Please try again.',
com_auth_error_create: '注册账户过程中出现错误,请重试。',
com_auth_full_name: '姓名',
com_auth_name_required: '姓名为必填项',
com_auth_name_min_length: 'Name must be at least 3 characters',
com_auth_name_max_length: 'Name must be less than 80 characters',
com_auth_name_min_length: '姓名至少3个字符',
com_auth_name_max_length: '姓名最多80个字符',
com_auth_username: '用户名',
com_auth_username_required: 'Username is required',
com_auth_username_min_length: 'Username must be at least 3 characters',
com_auth_username_max_length: 'Username must be less than 20 characters',
com_auth_username_required: '用户名为必填项',
com_auth_username_min_length: '用户名至少3个字符',
com_auth_username_max_length: '用户名最多20个字符',
com_auth_already_have_account: '已有账号',
com_auth_login: '登录',
com_auth_reset_password: '重置密码',
com_auth_click: '点击',
com_auth_here: '这里',
com_auth_to_reset_your_password: '重置密码.',
com_auth_error_reset_password:
'There was a problem resetting your password. There was no user found with the email address provided. Please try again.',
com_auth_error_reset_password: '重置密码出现错误,未找到对应的邮箱地址,请重新输入。',
com_auth_reset_password_success: '密码重置成功',
com_auth_login_with_new_password: '现在你可以使用你的新密码登录.',
com_auth_error_invalid_reset_token: 'This password reset token is no longer valid.',
com_auth_error_invalid_reset_token: '重置密码的密钥已失效。',
com_auth_click_here: '点击这里',
com_auth_to_try_again: '再试一次.',
com_auth_submit_registration: '注册提交',
com_auth_welcome_back: '欢迎',
com_endpoint_bing_enable_sydney: '启用 Sydney',
com_endpoint_bing_to_enable_sydney: '启用 Sydney',
com_endpoint_bing_jailbreak: '破解',
com_endpoint_bing_context_placeholder:
'Bing can use up to 7k tokens for \'context\', which it can reference for the conversation. The specific limit is not known but may run into errors exceeding 7k tokens',
com_endpoint_bing_system_message_placeholder:
'WARNING: Misuse of this feature can get you BANNED from using Bing! Click on \'System Message\' for full instructions and the default message if omitted, which is the \'Sydney\' preset that is considered safe.',
com_endpoint_system_message: '系统消息',
com_endpoint_default_blank: '初始值: 空',
com_endpoint_default_false: '初始值: false',
com_endpoint_default_creative: '初始值: creative',
com_endpoint_default_empty: '初始值: empty',
com_endpoint_default_with_num: '初始值: {0}',
com_endpoint_context: '上下文',
com_endpoint_tone_style: '语气',
com_endpoint_token_count: '词元数',
com_endpoint_output: '输出',
com_endpoint_google_temp:
'值越高表示输出越随机值越低表示输出越确定。建议不要同时改变此值和Top P。',
com_endpoint_google_topp:
'Top-p changes how the model selects tokens for output. Tokens are selected from most K (see topK parameter) probable to least until the sum of their probabilities equals the top-p value.',
com_endpoint_google_topk:
'Top-k changes how the model selects tokens for output. A top-k of 1 means the selected token is the most probable among all tokens in the model\'s vocabulary (also called greedy decoding), while a top-k of 3 means that the next token is selected from among the 3 most probable tokens (using temperature).',
com_endpoint_google_maxoutputtokens:
' Maximum number of tokens that can be generated in the response. Specify a lower value for shorter responses and a higher value for longer responses.',
com_endpoint_google_custom_name_placeholder: '为PaLM2设置一个名称',
com_endpoint_google_prompt_prefix_placeholder: '自定义指令和上下文,默认为空。',
com_endpoint_custom_name: '自定义名称',
com_endpoint_prompt_prefix: '对话前缀',
com_endpoint_temperature: '温度',
com_endpoint_default: '初始值',
com_endpoint_top_p: 'Top P',
com_endpoint_top_k: 'Top K',
com_endpoint_max_output_tokens: '最大输出词元数',
com_endpoint_openai_temp:
'值越高表示输出越随机值越低表示输出越确定。建议不要同时改变此值和Top P。',
com_endpoint_openai_max: '最大生成词元数。输入词元长度由模型的上下文长度决定。',
com_endpoint_openai_topp:
'相较于温度的另一个取样方法称为核采样模型选取输出词元中大于P值概率密度在整个概率分布中的比例的结果。比如 top_p=0.1 表示只有概率占比为前10%的词元才会被考虑作为输出。建议不要同时改变此值和温度。',
com_endpoint_openai_freq:
'值介于-2.0到2.0之间。正值表示根据已有词元的频率惩罚重复词元结果, 从而减少模型输出重复词。',
com_endpoint_openai_pres:
'值介于-2.0到2.0之间。正值表示根据出现词元惩罚重复词元结果, 从而增加模型提出新主题的可能性。',
com_endpoint_openai_custom_name_placeholder: '为ChatGPT设置一个名称',
com_endpoint_openai_prompt_prefix_placeholder: '可以在系统消息中设置自定义指令,默认为空',
com_endpoint_frequency_penalty: '频度惩罚',
com_endpoint_presence_penalty: '出现惩罚',
com_endpoint_plug_use_functions: '使用函数',
com_endpoint_plug_skip_completion: '跳过补全',
com_endpoint_disabled_with_tools: '系统禁用',
com_endpoint_disabled_with_tools_placeholder: '系统禁用',
com_endpoint_plug_set_custom_instructions_for_gpt_placeholder:
'可以在系统消息中设置自定义指令,默认为空',
com_endpoint_set_custom_name: '设置一个自定义名,以方便寻找预设',
com_endpoint_preset_name: '预设名',
com_endpoint: '端口',
com_endpoint_hide: '隐藏',
com_endpoint_show: '显示',
com_endpoint_examples: ' 例子',
com_endpoint_completion: '补全',
com_endpoint_agent: '代理',
com_endpoint_show_what_settings: '显示{0}的设置',
com_endpoint_save: '保存',
com_endpoint_export: '导出',
com_endpoint_save_as_preset: '保存为预设',
com_endpoint_not_implemented: '未实现功能',
com_endpoint_edit_preset: '预设编辑',
com_endpoint_view_options: '查看选项',
com_endpoint_my_preset: '我的预设',
com_endpoint_agent_model: '代理模型 (推荐: GPT-3.5)',
com_endpoint_completion_model: '补全模型 (推荐: GPT-4)',
com_endpoint_func_hover: '将插件当做OpenAI函数使用',
com_endpoint_skip_hover: '跳过补全步骤, 用于检查最终答案和生成步骤',
com_nav_export_filename: '文件名',
com_nav_export_filename_placeholder: '设置文件名',
com_nav_export_type: '类型',
com_nav_export_include_endpoint_options: '包含配置信息',
com_nav_enabled: '打开',
com_nav_not_supported: '未支持',
com_nav_export_all_message_branches: '导出所有对话',
com_nav_export_recursive_or_sequential: '递归或顺序?',
com_nav_export_recursive: '递归',
com_nav_export_conversation: '导出对话',
com_nav_theme: '主题',
com_nav_theme_system: '系统',
com_nav_theme_dark: '暗',
com_nav_theme_light: '亮',
com_nav_clear: '清空',
com_nav_clear_all_chats: '清空所有对话',
com_nav_confirm_clear: '确认清空',
com_nav_close_sidebar: '关闭侧边栏',
com_nav_open_sidebar: '打开侧边栏',
com_nav_log_out: '登出',
com_nav_user: '默认用户',
com_nav_clear_conversation: '清空对话',
com_nav_clear_conversation_confirm_message: '请确认是否清空所有对话?此操作无法撤回。',
com_nav_help_faq: '帮助',
com_nav_settings: '设置',
com_nav_search_placeholder: '文本搜索',
com_nav_setting_general: '通用',
};