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>